21 Commits
Author SHA1 Message Date
Claudecio Martins 539d5e4c33 fix: improve SVG text alignment and positioning support in AssetManager 2026-10-12 13:44:31 -03:00
Claudecio Martins 5551d9e667 docs: add SVG design guide and update PDF and XML asset files 2026-10-12 13:40:07 -03:00
Claudecio Martins ef44d34944 refactor: redesign main action grid to 3x2 layout and update related UI components and icons 2026-10-12 13:26:20 -03:00
Claudecio Martins edccfff7ff feat: add exit confirmation dialog, system tray enhancements, and diagnostic options 2026-10-12 12:32:35 -03:00
Claudecio Martins 16212702cb fix: add specific user JDK path to fallback list in jar build script 2026-10-12 10:52:35 -03:00
Claudecio Martins 6f191c6a69 feat: copy built signer jar to sdk directory during build process 2026-10-12 10:43:53 -03:00
Claudecio Martins d1cfab95c2 feat: improve UI startup performance, window focus handling, and system tray interaction with tests 2026-10-12 10:37:36 -03:00
Claudecio Martins 64ca13b3a1 fix: enhance security with strict file permissions, pin isolation, and robust protocol validations 2026-10-11 21:47:35 -03:00
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
Claudecio Martins c997afbfd4 fix(pdf): estabiliza drag do selo, remove razao/local e elimina reticencias em botoes e avisos 2026-10-11 10:17:44 -03:00
Claudecio Martins 7f6d8a4a08 feat(pdf): centraliza configuracao de selo padrao e adiciona armazenamento seguro de PIN 2026-10-11 10:00:46 -03:00
111 changed files with 10143 additions and 1899 deletions
+111 -74
View File
@@ -1,100 +1,137 @@
# 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
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-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-app](modules/cybercore-app)**: Ponto de entrada da aplicação (`CyberCoreApp`), orquestrador dos serviços de ciclo de vida e servidor CCSP local.
* **[sdk/](sdk)**: SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração de aplicações web com o agente desktop.
* **[docs/](docs)**: Documentação técnica detalhada de arquitetura, protocolo, segurança e casos de uso.
* **[`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 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, diagnóstico pré-inicialização e servidor CCSP local.
* **[`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 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.).
* **Maven:** 3.9+ (ou utilizar o wrapper `./mvnw` incluído no projeto).
* **Sistema Operacional Alvo:** Windows 10/11 ou Linux (x86_64).
| Recurso | Padrão / Tecnologia | Descrição |
|---|---|---|
| **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
Para compilar e gerar o JAR consolidado (com todas as dependências sombreadas e assets):
```bash
./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
```
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.
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.
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.
---
## 🔒 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.
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.
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.
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:
* 🌐 **[Protocolo CCSP/1](docs/CCSP.md):** Contrato REST, endpoints, códigos de status e anti-replay.
* 📦 **[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:
* [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.
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.
+31 -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
│ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── 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)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
└── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
├── app-icon.png # Fallback em PNG (256x256 px)
├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
├── 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,22 @@ assets/
## 🎨 Como Customizar Qualquer Ícone ou Imagem
1. **Ícones:**
- Estão no formato `.svg` padrão.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou 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.
- Todos estão no formato `.svg` padrão da W3C.
- Consulte o guia completo de especificações em [docs/svg-design-guide.md](../docs/svg-design-guide.md) para tokens de cores, dimensões e boas práticas ao exportar do **Inkscape/Figma**.
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
- 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:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
- 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.
- 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 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:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
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!
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
3. **Ordem de Resolução e Carregamento:**
Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
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/`)**: Para customizações no diretório pessoal do usuário.
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 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">
<!-- Lua crescente ciano/índigo -->
<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"/>
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/>
<circle cx="42" cy="18" r="1" fill="#818cf8"/>
<!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
<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="34" cy="14" r="1.8" fill="#818cf8"/>
<circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 344 B

+6 -6
View File
@@ -4,10 +4,10 @@
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta vermelha com sigla PDF -->
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8.5" font-weight="bold" fill="#ffffff" text-anchor="middle">PDF</text>
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta vermelha com sigla PDF perfeitamente centralizada -->
<rect x="11" y="31" width="28" height="11" rx="3" fill="#d32f2f"/>
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8.5px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">PDF</text>
</svg>

Before

Width:  |  Height:  |  Size: 996 B

After

Width:  |  Height:  |  Size: 1.0 KiB

+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">
<!-- Engrenagem simétrica de 8 dentes 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"/>
<!-- 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"/>
<!-- Engrenagem corporativa simétrica centrada em (24, 24) -->
<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"/>
</svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 784 B

+6 -6
View File
@@ -4,10 +4,10 @@
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta azul com sigla XML -->
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8" font-weight="bold" fill="#ffffff" text-anchor="middle">&lt;XML&gt;</text>
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta azul com sigla XML perfeitamente centralizada -->
<rect x="11" y="31" width="28" height="11" rx="3" fill="#0288d1"/>
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">&lt;XML&gt;</text>
</svg>

Before

Width:  |  Height:  |  Size: 998 B

After

Width:  |  Height:  |  Size: 1.0 KiB

+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

+8 -1
View File
@@ -55,6 +55,7 @@ echo -e "${COLOR_RESET}"
# 1. Verificação e Auto-Detecção do Java (JDK 21+)
if ! command -v java &>/dev/null; then
POSSIBLE_JDKS=(
"${HOME}/.jdks/jdk-25.0.4.1+1"
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
"/usr/lib/jvm/java-25-openjdk-amd64"
"/usr/lib/jvm/java-21-openjdk-amd64"
@@ -105,9 +106,15 @@ if [[ ! -f "${SOURCE_JAR}" ]]; then
exit 1
fi
# 4. Copia para a raiz para facilitar o acesso
# 4. Copia para a raiz e para a pasta sdk/ para manter o ambiente sincronizado
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
if [[ -d "sdk" ]]; then
SDK_JAR="sdk/cybercore-signer.jar"
cp -f "${SOURCE_JAR}" "${SDK_JAR}"
echo -e "Cópia sincronizada para o SDK: ${COLOR_BOLD}${SDK_JAR}${COLOR_RESET}"
fi
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
echo ""
+294 -108
View File
@@ -1,28 +1,55 @@
# 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).
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.
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.
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`).
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.
O protocolo CCSP/1 adota o princípio de menor privilégio e segurança em profundidade:
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).
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`.
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`).
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`
Verifica se o agente está instalado, em execução e pronto para operações.
| Método | Endpoint | Finalidade | Autenticação Requerida |
|---|---|---|---|
| `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
{
"version": "1",
@@ -30,20 +57,58 @@ Verifica se o agente está instalado, em execução e pronto para operações.
"agent": "CyberCore Signer",
"agent_version": "0.1.0",
"os": "Windows 11",
"timestamp": "2026-10-08T19:40:00Z"
"timestamp": "2026-10-11T20:45:00Z"
}
```
---
### 2.2 `GET /ccsp/v1/certificates`
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura.
### 3.2 `POST /ccsp/v1/authorize`
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
- `X-CyberCore-AppId`: UUID da aplicação autorizada (ou query param `?application_id=uuid`).
#### Cabeçalhos
- `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.
- `X-CyberCore-AppId` (Header) ou `?application_id=uuid` (Query Param).
#### Resposta de Sucesso (200 OK)
#### Resposta de Sucesso (`200 OK`)
```json
{
"version": "1",
@@ -51,16 +116,18 @@ Retorna a lista de certificados digitais atualmente disponíveis e aptos para as
"certificates": [
{
"id": "win-4340656a",
"alias": "CLAUDIO SILVA:12345678900",
"common_name": "CLAUDIO SILVA:12345678900",
"subject_dn": "CN=CLAUDIO SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
"issuer_dn": "CN=AC SERPRO v5, O=ICP-Brasil, C=BR",
"alias": "MARIA DA SILVA:12345678900",
"common_name": "MARIA DA SILVA:12345678900",
"subject_dn": "CN=MARIA DA SILVA:12345678900, OU=Autenticado por AR, 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",
"provider_id": "provider-win-store",
"device_name": "Token / Smart Card (Windows CAPI/CNG)",
"valid_from": "2024-05-10T12:00:00Z",
"valid_to": "2027-05-10T12:00:00Z",
"device_name": "Token USB / Smart Card (Windows CAPI/CNG)",
"valid_from": "2025-01-10T10:00:00Z",
"valid_to": "2028-01-10T10:00:00Z",
"serial_number": "72910482910482019482",
"cpf": "12345678900",
"cnpj": null,
"can_sign": true,
"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`
Handshake inicial de uma nova aplicação web para solicitar autorização ao usuário.
### 3.4 `POST /ccsp/v1/sign`
Solicita a assinatura digital local do digest informado com o certificado escolhido.
#### 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
#### Payload da Requisição
```json
{
"version": "1",
@@ -106,103 +150,245 @@ Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquiv
"algorithm": "SHA256withRSA",
"digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
"certificate_hint": "win-4340656a",
"document_name": "contrato_locacao.pdf",
"document_name": "contrato_prestacao_servicos.pdf",
"document_type": "PDF",
"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
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "signed",
"signature": "MEUCIQDq...base64...",
"certificate": "MIIFazC...base64...",
"signature": "MEUCIQDq95kZc...Base64...",
"certificate": "MIIFazCCBF...Base64...",
"certificate_chain": [
"MIIFazC...base64...",
"MIIEazC...base64..."
"MIIFazCCBF...Base64...",
"MIIEazCCA1...Base64..."
],
"algorithm": "SHA256withRSA",
"subject_name": "CLAUDIO SILVA",
"subject_name": "MARIA DA SILVA",
"subject_cpf_cnpj": "12345678900",
"document_name": "contrato_locacao.pdf",
"document_name": "contrato_prestacao_servicos.pdf",
"document_type": "PDF"
}
```
---
### 2.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).
### 3.5 `POST /ccsp/v1/authenticate`
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
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"request_id": "8f3b201a-8c9d-4e5f-b012-3456789abcde",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"operation": "authenticate",
"challenge": "server-session-challenge-nonce-2026",
"challenge": "session-nonce-random-server-token-998877",
"certificate_hint": "win-4340656a",
"purpose": "AUTHENTICATION",
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
"expires_at": "2026-10-08T19:45:00Z"
"nonce": "d3b07384d113edec49eaa6238ad5ff00",
"expires_at": "2026-10-11T20:50:00Z"
}
```
#### Resposta de Sucesso (200 OK)
#### Resposta de Sucesso (`200 OK`)
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"request_id": "8f3b201a-8c9d-4e5f-b012-3456789abcde",
"status": "authenticated",
"signature": "MEUCIQDq...base64...",
"certificate": "MIIFazC...base64...",
"signature": "MEQCIF61pP...Base64...",
"certificate": "MIIFazCCBF...Base64...",
"certificate_chain": [
"MIIFazC...base64..."
"MIIFazCCBF...Base64...",
"MIIEazCCA1...Base64..."
],
"algorithm": "SHA256withRSA",
"subject_name": "CLAUDIO SILVA",
"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."
"subject_name": "MARIA DA SILVA",
"subject_cpf_cnpj": "12345678900"
}
```
---
## 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 |
|---|---|
| `CERTIFICATE_NOT_FOUND` | Certificado indicado no hint não foi encontrado. |
| `CERTIFICATE_EXPIRED` | O certificado selecionado está fora do período de vigência. |
| `CERTIFICATE_INVALID` | O certificado é inválido ou corrompido. |
| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível ou ausente. |
| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. |
| `TOKEN_REMOVED` | Dispositivo de hardware foi removido durante a assinatura. |
| `PIN_REQUIRED` | PIN obrigatório para liberar chave privada em PKCS#11. |
| `PIN_INCORRECT` | PIN digitado está incorreto. |
| `PKCS11_ERROR` | Falha no middleware PKCS#11 do fabricante. |
| `SIGNATURE_FAILED` | Erro durante a geração da assinatura digital. |
| `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou recusada pelo usuário. |
| `REQUEST_EXPIRED` | A data/hora de validade da requisição expirou. |
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. |
| `INVALID_ORIGIN` | O cabeçalho Origin não corresponde ao cadastro da aplicação. |
| `UNSUPPORTED_ALGORITHM` | Algoritmo não suportado (ex: diferente de SHA256withRSA). |
| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela de confirmação. |
| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário está inativo no SO. |
| `COMMUNICATION_ERROR` | Erro interno no agente de comunicação local. |
#### Cabeçalhos
- `Origin: https://meusistema.empresa.com.br`
- `X-CyberCore-AppId: 550e8400-e29b-41d4-a716-446655440000`
#### Resposta de Sucesso (`200 OK`)
```json
{
"version": "1",
"status": "success",
"default_seal_id": "icp-brasil",
"seals": [
{
"id": "icp-brasil",
"name": "ICP-Brasil Institucional",
"description": "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
"primary_color": "#0d47a1",
"secondary_color": "#0284c7",
"border_color": "#93c5fd",
"background_color": "#ffffff",
"text_color": "#0f172a",
"icon_style": "shield",
"width": 320,
"height": 82,
"default_cpf_mode": "FULL",
"default_include_date_time": true
},
{
"id": "gov-br",
"name": "Governamental (Padrão Gov.br / ITI)",
"description": "Faixa verde e ouro com declaração expressa de validade pela MP nº 2.200-2/2001",
"primary_color": "#047857",
"secondary_color": "#eab308"
}
]
}
```
---
### 3.7 `POST /ccsp/v1/sign-pdf`
Recebe um documento PDF em Base64, a página e as coordenadas desejadas (`x`, `y`, `scale`), aplica o selo visual e a assinatura digital PAdES (ISO 32000-1 / RFC 5652) e retorna o PDF assinado completo em Base64.
#### Payload da Requisição
```json
{
"version": "1",
"request_id": "b1b74288-7264-44a5-9271-bf10d32c5f11",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"operation": "sign-pdf",
"certificate_hint": "cert-1",
"pdf": "JVBERi0xLjQKJ...Base64...",
"page": 1,
"x": 60.0,
"y": 60.0,
"scale": 1.0,
"visible_seal": true,
"seal_theme_id": "icp-brasil",
"reason": "Aprovado e assinado digitalmente",
"location": "Curitiba, PR",
"nonce": "c301d011-8012-4261-bcfa-e7912ab5a420"
}
```
> **Selo Customizado Inline (`seal_custom`):**
> Em vez de `seal_theme_id`, a requisição pode fornecer um objeto `seal_custom` com cores hexadecimais, título customizado, ícone vetorial (`shield`, `certificate`, `lock`) e política de privacidade LGPD (`FULL`, `MASKED`, `HIDDEN`).
#### Resposta de Sucesso (`200 OK`)
```json
{
"version": "1",
"request_id": "b1b74288-7264-44a5-9271-bf10d32c5f11",
"status": "signed",
"signed_pdf": "JVBERi0xLjQKJ...PDF_ASSINADO_BASE64...",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"certificate": "MIIFazCCBF...Base64...",
"algorithm": "SHA256withRSA"
}
```
---
### 3.8 `POST /ccsp/v1/sign-xml`
Executa a assinatura digital de documentos XML em conformidade com o padrão XMLDSig W3C ou XAdES-BES ICP-Brasil (Padrão NF-e / NFC-e / CT-e / NFS-e). O documento assinado completo é devolvido diretamente ao sistema web.
#### Payload da Requisição
```json
{
"version": "1",
"request_id": "e44d32e1-455b-4357-9351-41c10d3b6f22",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"operation": "sign-xml",
"certificate_hint": "cert-1",
"xml": "<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
```
+65
View File
@@ -0,0 +1,65 @@
# Diagnóstico: Janela Principal no Windows 11 (Bisseção e Monitoramento)
Este documento descreve o procedimento de diagnóstico para isolar a causa de ocultamento nativo da janela principal no Windows 11 ao passar o mouse.
---
## 1. Monitor Nativo Win32/DWM (`scripts/diag-window.ps1`)
O script [scripts/diag-window.ps1](file:///var/www/cybercore/cybercore-signer/scripts/diag-window.ps1) inspeciona a cada 300ms a janela nativa via P/Invoke da Win32 API e DWM:
- `IsWindowVisible`: se a janela está visível perante o subsistema Win32;
- `DWMWA_CLOAKED`: se o gerenciador gráfico do Windows 11 ocultou a composição;
- `IsIconic`: se foi minimizada;
- `GetWindowRect`: coordenadas reais (detecta se foi deslocada para fora da tela).
### Como executar o monitor:
Em uma janela separada do PowerShell:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
```
---
## 2. Roteiro de Bisseção (Fase 2)
Execute cada um dos comandos abaixo individualmente (garantindo antes que instâncias anteriores foram encerradas com `Stop-Process -Name java -Force` se necessário):
### Teste A: Desativar Middleware PKCS#11 / SafeSign (`aetpkss1.dll`)
Isola hooks nativos do SafeSign e middlewares A3:
```powershell
java -Dcybercore.diag.no-pkcs11=true -jar .\cybercore-signer.jar --dev
```
*Se a janela NÃO sumir ao passar o mouse, a causa é o hook nativo do middleware PKCS#11.*
---
### Teste B: Plain Stage (Janela livre sem `setResizable(false)` e sem limites min/max)
Isola o bug de composição do DWM / Glass no Windows 11 com janelas de tamanho fixo:
```powershell
java -Dcybercore.diag.plain-stage=true -jar .\cybercore-signer.jar --dev
```
*Se a janela NÃO sumir ao passar o mouse, a causa é a restrição fixa de tamanho no Windows 11.*
---
### Teste C: Desativar Efeitos de Sombra no Mouse (`-fx-effect`)
Isola recálculo de dropshadow e troca de cursor:
```powershell
java -Dcybercore.diag.no-effects=true -jar .\cybercore-signer.jar --dev
```
---
### Teste D: Desativar Monitor Dinâmico de Smart Cards (PC/SC)
Isola threads em segundo plano de `javax.smartcardio`:
```powershell
java -Dcybercore.diag.no-smartcard-watcher=true -jar .\cybercore-signer.jar --dev
```
---
### Teste E: Desativar Windows Certificate Store (Windows-MY)
Isola inicialização de CryptoAPI / SunMSCAPI:
```powershell
java -Dcybercore.diag.no-wincert=true -jar .\cybercore-signer.jar --dev
```
+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
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)
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. O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou importa via tela de 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.
3. O provedor `Pkcs12CertificateProvider` instancia um `KeyStore.getInstance("PKCS12")` e extrai os metadados do certificado X.509 (`subject`, `issuer`, `validade`, `algoritmo`, `tamanho da chave`).
4. Durante a assinatura, a chave privada em memória gera a assinatura digital via `Signature.getInstance("SHA256withRSA")`.
### 1.1 Arquitetura e Ciclo de Vida
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. **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. **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. **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)
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`
- Acessa certificados físicos via CryptoAPI (CAPI) e Cryptography Next Generation (CNG) através do provedor JDK `SunMSCAPI`.
- A chave privada permanece dentro do chip criptográfico do 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.
### 2.1 Funcionamento do `WindowsCertificateStoreProvider`
* **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.
* **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).
* **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:
- `Pkcs11CertificateProvider` pesquisa bibliotecas DLL / SO padrão:
- `eps2003csp11.dll` (SafeNet / eToken)
- `aetpkss1.dll` (SafeSign AET)
- `acospkcs11.dll` (ACOS / G&D)
- `opensc-pkcs11.dll` / `opensc-pkcs11.so` (OpenSC)
- Utiliza o provider SunPKCS11 do JDK (`jdk.crypto.cryptoki`).
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`).
### 3.1 Mapeamento de Middlewares Homologados no Brasil
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):
| 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`)
O componente `SmartCardWatcher` monitora os leitores PC/SC em segundo plano:
- **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`.
O componente `SmartCardWatcher` executa um loop em segundo plano baseado na API padrão `javax.smartcardio` (subsistema PC/SC):
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
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
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/
@@ -19,43 +21,75 @@ cybercore-signer/
└── docs/ # Especificações técnicas e manuais de integração
```
### 1.1 `cybercore-core`
- **Domínio:** Modelos imutáveis como `ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType` e `CertificateStatus`.
- **Criptografia:** `SigningService` e `DigestAlgorithm` utilizando exclusivamente os provedores padrão JCA/JCE (sem reimplementação manual de algoritmos).
- **Provedores:** Abstração `CertificateProvider` com implementações:
- `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória protegida.
- `WindowsCertificateStoreProvider`: integração nativa com o repositório de chaves do Windows (`Windows-MY` via SunMSCAPI/CNG).
- `Pkcs11CertificateProvider`: integração com drivers de middleware e tokens USB/Smartcards via `SunPKCS11`.
- **Protocolo & Rede Local:** `CcspServer` escuta estritamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1.
- **Segurança & Configurações:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` (persistência em `~/.cybercore/config.json`).
- **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).
- **Selos Visuais Personalizados:** `CustomSealStorage` (persistência em `~/.cybercore/seals/`), `PdfSealThemeRegistry` dinâmico e `CpfDisplayMode` (privacidade LGPD).
### 1.1 Módulo `cybercore-core` (Agnóstico de UI)
O módulo de núcleo é totalmente independente de JavaFX ou bibliotecas visuais:
* **Domínio Imutável:** Modelos imutáveis (`ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType`, `CertificateStatus`).
* **Serviços de Assinatura:** `SigningService` orquestra a aplicação de algoritmos certificados (`SHA256withRSA`) delegando a operação ao provedor de certificado resolvido.
* **Provedores de Certificados (`CertificateProvider`):** Abstração que unifica:
- `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória com limpeza volátil de senha.
- `WindowsCertificateStoreProvider`: integração com o repositório nativo do Windows (`Windows-MY` via SunMSCAPI/CNG).
- `Pkcs11CertificateProvider`: integração direta com bibliotecas dinâmicas (DLL/SO) de drivers e tokens USB através do SunPKCS11.
* **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.
* **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/`).
* **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`
- Interface desenvolvida com JavaFX 25, empregando layout moderno e simétrico de grade 3x3 de operações e coluna lateral de utilitários:
- `MainAppView`: grade 3x3 otimizada:
- Linha 0: Assinar PDF, Assinar XML, Assinatura Atachada
- Linha 1: Co-Assinar Documento, Carimbo do Tempo, Gerenciar Selos
- Linha 2: Validar Assinatura, Validar Carimbo, Sair do Agente
- `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.2 Módulo `cybercore-ui` (Apresentação JavaFX)
Responsável por toda a interação com o usuário:
* **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade de operações rápidas (PDF, XML, Assinatura Atachada, Designer de Selos, Validar Assinatura, Sair do Agente) e coluna lateral de utilitários (Logs em tempo real, Configurações, Tutorial, Certificados e Sobre).
* **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`).
* **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.
* **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
### 1.3 `cybercore-app`
- Main bootstrap (`CyberCoreSignerApplication`).
- Tratamento de parâmetros de inicialização (`--dev`, `--headless`).
- Execução de diagnósticos preliminares de ambiente e engine criptográfica antes do início das threads de escuta.
- Geração de Fat JAR autocontido via `maven-shade-plugin` e preparação para distribuição `jpackage`.
### 1.3 Módulo `cybercore-app` (Orquestração e Bootstrap)
Ponto de partida da aplicação (`CyberCoreApp`):
* Processa argumentos de linha de comando (`--dev`, `--headless`).
* Executa rotinas de diagnóstico pré-inicialização do ambiente Java e disponibilidade criptográfica.
* 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**:
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.
3. O documento completo nunca sai do navegador e a chave privada nunca sai do computador ou do hardware do usuário.
Para manter a interface gráfica sempre fluida e ágil, o CyberCore Signer utiliza um particionamento rígido de threads:
```mermaid
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
sequenceDiagram
@@ -66,14 +100,25 @@ sequenceDiagram
participant UI as CyberCore JavaFX
participant Crypto as SigningService & Providers
Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce)
CCSP->>CCSP: Valida autorização, nonce anti-replay e digest
Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce, expires_at)
CCSP->>CCSP: Valida autorização, nonce anti-replay e integridade do digest
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
UI->>Usuario: Exibe Diálogo (Hash, Certificado, Aplicação)
Usuario->>UI: Aprova assinatura (e insere PIN se A3)
UI->>Usuario: Exibe Diálogo Nativo (Nome do Documento, Hash SHA-256, Certificado)
Usuario->>UI: Clica em "Autorizar Assinatura" (e digita PIN se requerido)
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-->>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
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
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)
Logs detalhados ativos. Operação com certificados reais.
CYBERCORE SIGNER — MODO DE DESENVOLVIMENTO ATIVADO (--dev)
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:**
- Adicione um certificado A1 `.pfx` ou `.p12` emitido por uma autoridade de teste ou autoassinado.
- Pela interface desktop, clique no botão **"Importar A1 (.pfx/.p12)"** no Dashboard e informe o arquivo e a senha.
- O certificado ficará disponível imediatamente para todas as operações CCSP/1.
### Passo 1: Gerar a Chave Privada RSA (2048 bits)
```bash
openssl genrsa -out teste_signer.key 2048
```
2. **Repositório Nativo do Windows (Windows-MY):**
- Instale o certificado no repositório de certificados pessoais do Windows (`certmgr.msc`).
- O provedor `WindowsCertificateStoreProvider` detectará o certificado automaticamente.
### Passo 2: Gerar o Certificado X.509 Autoassinado
```bash
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):**
- Conecte o dispositivo físico com middleware homologado (SafeSign, GD Burti, SafeNet, OpenSC, etc.).
- O provedor `Pkcs11CertificateProvider` detectará o token e seus slots.
### Passo 3: Exportar o Certificado e Chave no Contêiner PKCS#12 (`.pfx`)
```bash
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.
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.
O repositório do CyberCore Signer conta com suíte abrangente de testes unitários, testes de concorrência e testes de arquitetura (ArchUnit):
```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);
- Vendor do JDK (Temurin, Oracle, etc.);
- Arquitetura do processador e sistema operacional.
- Vendor do JDK (Eclipse Temurin, Oracle OpenJDK, Amazon Corretto);
- Arquitetura da CPU (x86_64 / amd64) e sistema operacional (Windows 10/11 ou Linux).
2. **Engine Criptográfica JCA/JCE:**
- Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.);
- Suporte ao algoritmo de hash `SHA-256`;
- Suporte ao algoritmo de assinatura `SHA256withRSA`.
3. **Repositório de Chaves do Sistema Operacional:**
- Detecção do repositório `Windows-MY` (Windows CryptoAPI / CNG).
4. **Middlewares PKCS#11 e SmartCard:**
- Verificação de bibliotecas de smartcard registradas no sistema (`eps2003csp11.dll`, `aetpkss1.dll`, `opensc-pkcs11.dll`, etc.);
- Disponibilidade do subsistema PC/SC `javax.smartcardio`.
- Suporte aos algoritmos de hash criptográfico (`SHA-256`, `SHA-512`);
- Suporte aos algoritmos de assinatura digital (`SHA256withRSA`).
3. **Repositório de Certificados do Sistema Operacional:**
- Detecção do repositório de chaves `Windows-MY` via SunMSCAPI / CryptoAPI / CNG.
4. **Middlewares PKCS#11 e Subsistema PC/SC:**
- Presença das bibliotecas dinâmicas do fabricante (`aetpkss1.dll`, `eps2003csp11.dll`, `acospkcs11.dll`, `opensc-pkcs11.dll`);
- 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)
A aba de diagnóstico disponibiliza o botão **"Executar Auto-teste de Assinatura"**:
1. Seleciona o primeiro certificado operacional disponível na máquina.
2. Gera um hash SHA-256 interno de teste: `"Documento_Teste_CyberCore_<timestamp>"`.
3. Abre o diálogo de confirmação visual para validação do usuário.
4. Executa a assinatura real JCA/JCE e afere os bytes gerados.
5. Emite relatório de sucesso com tempo decorrido e tamanho da assinatura.
A tela de diagnóstico oferece a função **"Executar Auto-teste de Assinatura"**:
1. O agente identifica o primeiro certificado digital operacional conectado.
2. Gera um hash SHA-256 interno sintético para teste: `"Documento_Teste_CyberCore_<timestamp>"`.
3. Abre o diálogo nativo de confirmação visual para validação do usuário.
4. Executa a assinatura real através do provedor JCA/JCE correspondente (solicitando PIN se A3).
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
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 |
|---|---|---|
| `CERTIFICATE_NOT_FOUND` | O certificado indicado pelo `certificate_hint` não foi localizado. | Solicite 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_INVALID` | O certificado possui estrutura corrompida ou ainda não é válido. | Verificar a data e integridade do arquivo/dispositivo. |
| `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. |
| `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_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. |
| `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_INCORRECT` | O PIN digitado está incorreto. | Informar o usuário e alertar sobre o risco de bloqueio de PUK após 3 tentativas. |
| `PKCS11_ERROR` | Falha no driver ou biblioteca PKCS#11 do fabricante. | Verificar a instalação do SafeSign, SafeNet ou OpenSC. |
| `SIGNATURE_FAILED` | Erro durante a geração dos bytes da assinatura digital. | Tentar novamente ou reiniciar o agente desktop. |
| `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. |
| `REQUEST_EXPIRED` | O timestamp ou `expires_at` da requisição expirou. | Gerar nova requisição de assinatura com timestamp atualizado. |
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | Sempre gerar um UUID único 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. |
| `UNSUPPORTED_ALGORITHM` | Algoritmo solicitado não é suportado pelo agente ou certificado. | Utilizar algoritmo padrão homologado (`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. |
| `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. |
| `COMMUNICATION_ERROR` | Erro interno de socket ou timeout de rede local. | Verificar se a porta `21325` não está bloqueada por antivírus local. |
| Código de Erro (`CcspErrorCode`) | Status HTTP | Descrição do Erro | Ação Recomendada para a Aplicação Web |
|---|---|---|---|
| `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `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` | `400 Bad Request` | O algoritmo de hash ou assinatura solicitado não é suportado pelo provedor. | Utilizar os algoritmos padrão homologados (`SHA256withRSA`). |
| `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` | `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` | `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);
}
}
```
+75 -19
View File
@@ -1,43 +1,99 @@
# 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
- **Sistema Operacional Homologado:** Windows 10 (64-bit) ou Windows 11 (64-bit).
- **Compatibilidade Adicional:** Linux x86_64 e macOS (arquitetura preparada).
- **Hardware:** Mínimo de 4 GB RAM, 150 MB de espaço em disco.
- **Porta de Rede Local:** A porta TCP `21325` deve estar livre em `127.0.0.1`.
* **Sistema Operacional Alvo:** Windows 10 (64-bit) ou Windows 11 (64-bit).
* **Compatibilidade Adicional:** Distribuições Linux modernas x86_64 (Ubuntu 20.04+, Debian 11+, Fedora 38+, RHEL 9+).
* **Memória RAM:** Mínimo de 4 GB (Recomendado: 8 GB).
* **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`
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
scripts\package-windows.bat
```
### O que o script realiza:
1. Compila o projeto em modo release:
Ou execute manualmente os comandos do pipeline:
```cmd
:: 1. Compilação do projeto e geração do Fat JAR consolidado
mvnw clean package -DskipTests
:: 2. Geração do instalador nativo via jpackage
jpackage ^
--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
```
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.).
- Cria atalhos na Área de Trabalho e no Menu Iniciar.
- Gera um instalador padrão MSI ou EXE em `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`.
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.
4. O serviço local `http://127.0.0.1:21325` estará imediatamente disponível para atender as aplicações web autorizadas.
No ambiente Linux x86_64:
```bash
# 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
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):**
- Disponível apenas em sistemas operacionais Microsoft Windows (Windows 10/11).
- Em Linux e macOS, o provedor `WindowsCertificateStoreProvider` declara-se automaticamente indisponível (`isAvailable() == false`), operando via arquivos A1 ou PKCS#11 direto.
2. **Tokens A3 e Middlewares Proprietários:**
- 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.
3. **Bloqueio de Cartão por Tentativas de PIN:**
- 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.
1. **Repositório Nativo do Windows (`Windows-MY`):**
- O provedor `WindowsCertificateStoreProvider` depende exclusivamente das APIs Win32/CryptoAPI e CNG expostas pelo provedor `SunMSCAPI` do JDK.
- **Disponibilidade:** Suportado exclusivamente em Microsoft Windows (Windows 10 e Windows 11).
- **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.
2. **Arquitetura de CPU dos Middlewares Proprietários:**
- 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)**.
- 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:**
- 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.
2. **Operações Concorrentes no Mesmo Token Físico:**
- 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.
1. **Tokens A3 e Leitores de Smartcard Físicos:**
- 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.
- O agente detecta a presença da biblioteca e interage com o dispositivo através de interfaces padronizadas (PKCS#11 ou CAPI/CNG).
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`.
- **Algoritmo:** `SHA256withRSA` / PKCS#1 v1.5 com digest SHA-256.
- **Salva Incremental:** Preserva a integridade de assinaturas anteriores e do documento original sem quebrar certificações prévias.
---
## 5. Ferramentas Web do SDK (@cybercore/signer-sdk)
O SDK web fornece componentes visuais nativos em JavaScript vanilla para aplicações web:
### 5.1 `PdfStampPicker` (Posicionador Visual de Selo)
Permite que o usuário final suba um PDF no navegador, folheie páginas, selecione cantos predefinidos ou arraste a estampa visual interativamente. O componente mapeia as coordenadas do canvas para o sistema de coordenadas do PDFBox (ISO 32000, com origem no canto inferior esquerdo) e gera em tempo real o payload JSON para a requisição.
```javascript
import { PdfStampPicker } from '@cybercore/signer-sdk';
const picker = new PdfStampPicker({
container: '#picker-container',
pdfSource: pdfArrayBuffer, // ou URL / Base64
initialScale: 1.0,
onPositionChange: (pos) => {
console.log('Página:', pos.page, 'X:', pos.x, 'Y:', pos.y);
}
});
// Obtém parâmetros prontos para a requisição CCSP
const stampConfig = picker.getStampConfig();
```
### 5.2 `PdfSealDesigner` (Criador & Personalizador de Selos)
Componente interativo com live preview em SVG vetorial para personalização de selos:
- Paleta de cores primária, secundária, fundo e texto.
- Ícones vetoriais (`shield`, `certificate`, `lock`) e upload de logotipo em Base64.
- Regras LGPD para mascaramento ou ocultação de CPF/CNPJ.
- Exportação da configuração JSON para envio inline via `seal_custom` na requisição `signPdfDocument()`.
### 5.3 Assinatura Completa via Base64 (`signPdfDocument`)
Permite enviar o PDF completo em Base64 e receber de volta o PDF assinado completo em Base64 para envio direto ao backend:
```javascript
const result = await signer.signPdfDocument({
certificateId: cert.id,
pdf: pdfArrayBuffer, // ou base64
page: stampConfig.page,
x: stampConfig.x,
y: stampConfig.y,
scale: stampConfig.scale,
seal_theme_id: 'icp-brasil', // ou seal_custom: customSealConfig
reason: 'Documento homologado',
location: 'São Paulo, SP'
});
console.log('PDF Assinado Base64:', result.signed_pdf);
console.log('SHA-256 do Documento Final:', result.sha256);
```
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)
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
import { CyberCoreSigner } from '@cybercore/signer-sdk';
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
### `isInstalled(timeoutMs = 2000): Promise<boolean>`
Testa rapidamente se o agente desktop está ativo na porta local `21325`.
### 2.1 Verificação de Disponibilidade e Conexão
### `connect(): 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.
### `getCertificates(): Promise<Array<Object>>`
Retorna todos os certificados A1 e A3 disponíveis e operacionais na máquina do usuário.
#### `isInstalled(timeoutMs = 2000): Promise<boolean>`
Testa de forma rápida e silenciosa se o agente desktop está ativo na máquina do usuário.
```javascript
const certs = await signer.getCertificates();
certs.forEach(cert => {
console.log(`[${cert.type}] ${cert.common_name} (Validade: ${cert.valid_to})`);
const disponivel = await signer.isInstalled();
if (!disponivel) {
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>`
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.
### 2.3 Operações de Assinatura Digital
#### `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
const pdfResult = await signer.signPdf(fileInput.files[0], {
certificateId: certs[0].id
const resultado = await signer.sign({
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>`
Assina um documento XML (XMLDSig / NF-e). Aceita string com o XML formatado ou objeto de arquivo `File`/`Blob`.
#### `signPdf(file, options): Promise<SignatureResult>`
Calcula o hash do documento PDF no próprio navegador e solicita a assinatura ao CyberCore Signer.
```javascript
const xmlResult = await signer.signXml(xmlString, {
certificateId: certs[0].id,
const inputElement = document.getElementById('arquivo-pdf');
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'
});
```
### `signFile(file, options): Promise<Object>`
Assina qualquer arquivo (ZIP, DOCX, imagens, binários, executáveis) gerando pacote de assinatura detached / CAdES.
#### `signFile(file, options): Promise<SignatureResult>`
Assina arquivos genéricos de qualquer formato (ZIP, DOCX, XLSX, imagens, binários).
```javascript
const fileResult = await signer.signFile(fileInput.files[0], {
certificateId: certs[0].id
const fileInput = document.getElementById('input-qualquer-arquivo');
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
const auth = await signer.authenticate({
certificateId: certs[0].id,
challenge: 'server-nonce-12345'
// 1. Obter desafio único do backend da sua aplicação
const { challenge } = await fetch('/api/auth/challenge').then(res => res.json());
// 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);
console.log('CPF/CNPJ:', auth.subjectCpfCnpj);
console.log('Assinatura do Desafio:', auth.signature);
// 3. Enviar a assinatura e o certificado para o backend validar e autenticar a sessão
const response = await fetch('/api/auth/validate-login', {
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
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
import {
UserCancelledError,
CertificateNotFoundError,
PinIncorrectError,
TokenNotFoundError
TokenNotFoundError,
CertificateNotFoundError,
CertificateExpiredError
} from '@cybercore/signer-sdk/errors';
try {
await signer.sign({ certificateId: 'cert-1', data: 'Contrato' });
} catch (err) {
if (err instanceof UserCancelledError) {
alert('Você cancelou a assinatura no CyberCore Signer.');
} else if (err instanceof PinIncorrectError) {
alert('O PIN do token informado está incorreto.');
} else if (err instanceof TokenNotFoundError) {
alert('Conecte o seu token USB na máquina para assinar.');
const res = await signer.sign({
certificateId: certId,
digest: documentoDigest
});
} catch (error) {
if (error instanceof UserCancelledError) {
// Usuário cancelou a assinatura voluntariamente na janela nativa
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 {
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
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`.
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.
### 1.1 Ligação Estrita ao Loopback (`127.0.0.1`)
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
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`):**
- A aplicação web identifica-se enviando 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.
2. **Armazenamento Seguro das Autorizações:**
- As aplicações autorizadas são gravadas em `~/.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.
1. **Handshake de Aprovação (`POST /ccsp/v1/authorize`):**
- 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 exibe um modal nativo no desktop informando o nome da aplicação e solicitando autorização do usuário.
2. **Armazenamento Seguro de Permissões:**
- As aplicações autorizadas são gravadas no arquivo local `~/.cybercore/authorized-apps.json`.
- 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
Cada solicitação de assinatura enviada pelo SDK deve incluir:
- Um `nonce` (UUID criptograficamente aleatório);
- Um campo `expires_at` (ou `timestamp`).
O componente `NonceManager`:
- Armazena os nonces recentes em memória protegida com expiração temporal (TTL);
- Rejeita com código estruturado `REPLAY_DETECTED` caso o mesmo nonce seja submetido mais de uma vez;
- Rejeita com código `REQUEST_EXPIRED` requisições cuja data/hora de validade já tenha sido ultrapassada.
Para impedir que requisições de assinatura capturadas por softwares maliciosos na máquina sejam reenviadas:
1. **Identificador Único (`nonce`):** Toda requisição ao `/ccsp/v1/sign` deve conter um UUID aleatório criptograficamente forte gerado pelo chamador.
2. **Janela de Validade Temporal (`expires_at`):** A requisição inclui uma estampa de tempo limite (TTL).
3. **Gerenciador de Nonces (`NonceManager`):**
- Mantém uma estrutura em memória com os identificadores recentes;
- 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`);
- Se a data/hora atual ultrapassar `expires_at`, a operação é rejeitada com erro `REQUEST_EXPIRED` (HTTP `422 Unprocessable Entity`).
---
## 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.
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.
3. **Proteção de PIN e Senhas:**
- 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.
- Em caso de remoção do hardware, o `SmartCardWatcher` limpa e zera os buffers de memória de PIN imediatamente.
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:** 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. **Higienização Segura de Memória Volátil:**
- O PIN de smartcards e senhas de certificados A1 nunca são convertidos para strings imutáveis do Java (`java.lang.String`).
- 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.
+6 -12
View File
@@ -30,24 +30,18 @@ Em conformidade com os requisitos do projeto, a interface **não é uma cópia d
---
## 3. Grade Central de Ações (Hub 4x3)
## 3. Grade Central de Ações (Hub 3x2)
A interface principal organiza as operações em 12 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
A interface principal organiza as operações em 6 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
| Posição | Operação | Descrição |
|---|---|---|
| (0, 0) | **Assinar PDF** | Assinatura digital de documentos PDF com cálculo de hash SHA-256 e gravação de assinatura. |
| (1, 0) | **Assinar XML** | Assinatura de documentos XML (estruturas fiscais, NF-e, XMLDSig). |
| (2, 0) | **Assinar Arquivo** | Assinatura genérica de qualquer arquivo ou binário do sistema. |
| (3, 0) | **Assinar em Lote** | Processamento simultâneo de múltiplos arquivos com barra de progresso em tempo real. |
| (0, 1) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
| (1, 1) | **Co-Assinar Documento** | Adição de nova assinatura a um documento previamente assinado por outro titular. |
| (2, 1) | **Carimbo do Tempo** | Integração com protocolo RFC 3161 TSP para prova de anterioridade temporal. |
| (3, 1) | **Assinatura LTV** | Perfil de Longa Duração com inclusão de cadeia de certificados e carimbos para validade permanente. |
| (0, 2) | **Validar Assinatura** | Verificação de integridade criptográfica e conformidade do certificado digital. |
| (1, 2) | **Validar Carimbo** | Verificação da autenticidade e validade temporal de tokens TSR/TSP. |
| (2, 2) | *(Reservado)* | Espaço reservado para expansões futuras do agente. |
| (3, 2) | **Sair do Agente** | Encerramento seguro da sessão do agente. |
| (2, 0) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
| (0, 1) | **Gerenciar Selos** | Abertura do Designer e Gerenciador de Selos Digitais visuais para PAdES. |
| (1, 1) | **Validar Assinatura** | Verificação de integridade criptográfica e conformidade do certificado digital. |
| (2, 1) | **Sair do Agente** | Encerramento seguro da sessão do agente. |
---
+86
View File
@@ -0,0 +1,86 @@
# Guia de Padrão Visual e Design System SVG — CyberCore Signer
Este documento estabelece a especificação oficial de design, geometria, tokens de cores e regras de compatibilidade para todos os ícones e elementos gráficos vetoriais (**SVG**) do ecossistema **CyberCore Signer**.
---
## 1. Princípios de Design
1. **Clareza e Precisão:** Cada ícone deve comunicar imediatamente sua função técnica (PDF, XML, assinatura embutida, validação, credenciais).
2. **Minimalismo e Geometria Pura:** Evitar ruídos visuais, nós desnecessários, filtros proprietários ou imagens bitmap embutidas (`<image>`).
3. **Consistência de Traço e Escala:** Espessuras de contorno previsíveis e alinhadas ao grid de 48x48 pixels.
4. **Interoperabilidade:** Totalmente compatível com editores vetoriais modernos (**Inkscape, Figma, Adobe Illustrator**) e com o parser nativo de JavaFX em `AssetManager`.
---
## 2. Grid e Canvas Canônico
| Propriedade | Valor Padrão | Descrição |
|---|---|---|
| **ViewBox** | `0 0 48 48` | Padrão obrigatório para ícones da grade de ações e ferramentas laterais. |
| **Dimensões Base** | `width="48" height="48"` | Dimensões nominais 1:1. |
| **Área Segura** | `4px` de margem interna | O conteúdo vetorial deve se concentrar entre `(4, 4)` e `(44, 44)`. |
| **Raio de Canto Padrão** | `rx="3"` ou `rx="6"` | Arredondamentos harmônicos em cantos de janelas e etiquetas. |
---
## 3. Paleta de Cores e Tokens Visuais
Os ícones utilizam uma paleta de cores corporativa baseada em Material Design e Tailwind Modern, com alto contraste sobre fundos claros e escuros:
| Token | Hexadecimal | Papel / Significado |
|---|---|---|
| **`color-brand-primary`** | `#0288d1` | Azul CyberCore. Traço padrão de folhas, botões principais e XML. |
| **`color-brand-deep`** | `#01579b` | Azul escuro para dobras e acentos de profundidade. |
| **`color-paper-fold`** | `#b3e5fc` | Preenchimento suave da dobra superior do documento padrão. |
| **`color-pdf-accent`** | `#d32f2f` | Vermelho institucional do formato Adobe PDF. |
| **`color-pdf-fold`** | `#ffcdd2` | Preenchimento suave da dobra do documento PDF. |
| **`color-success`** | `#10b981` / `#2e7d32` | Verde esmeralda para assinaturas válidas e verificações. |
| **`color-gold-seal`** | `#f59e0b` / `#d97706` | Dourado e âmbar para selos notariais e certificação digital. |
| **`color-paper-lines`** | `#b0bec5` / `#cfd8dc` | Linhas de texto representativas e divisórias neutras. |
| **`color-console-bg`** | `#1e293b` | Fundo de janelas de terminal (Logs). |
| **`color-danger-exit`** | `#d32f2f` | Seta e alertas de encerramento do agente. |
---
## 4. Anatomia dos Ícones de Documentos
Para documentos com dobra (como PDF, XML, Atachada e Validador):
```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- 1. Folha de documento com dobra -->
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z"
fill="#ffffff" stroke="[COR_BORDA]" stroke-width="2.5" stroke-linejoin="round"/>
<!-- 2. Dobra de canto superior direito -->
<path d="M 32 4 L 32 14 L 42 14 Z"
fill="[COR_FUNDO_DOBRA]" stroke="[COR_BORDA]" stroke-width="2" stroke-linejoin="round"/>
<!-- 3. Linhas de texto representativas -->
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- 4. Elemento distintivo inferior (Etiqueta, Caneta, Clipe ou Checkmark) -->
<rect x="11" y="31" width="28" height="11" rx="3" fill="[COR_BADGE]"/>
<text x="25" y="39" font-family="'Segoe UI', Arial, sans-serif"
font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle">[TEXTO]</text>
</svg>
```
---
## 5. Boas Práticas para Exportação no Inkscape / Figma
Ao criar ou editar ícones nas ferramentas vetoriais externas:
1. **Escolha a opção "SVG Simples" (Plain SVG) no Inkscape:**
- No Inkscape: `Arquivo → Salvar Como... → SVG Simples (*.svg)`.
- Isso elimina metadados de sessão (`sodipodi:namedview`, guias, zoom e histórico).
2. **Nunca incorpore imagens bitmap (`<image>` base64):**
- O aplicativo desktop deve conter exclusivamente geometria vetorial limpa (`path`, `rect`, `circle`, `line`, `polygon`, `polyline`, `text`).
3. **Suporte a `style="..."` inline:**
- O parser do CyberCore Signer (`AssetManager`) suporta tanto atributos XML puros (`fill="#..."`) quanto estilos inline em CSS (`style="fill:#...;stroke:#..."`).
4. **Mantenha o ViewBox Quadrado:**
- Preserve sempre `viewBox="0 0 48 48"` para garantir alinhamento perfeito nos botões e painéis da interface.
+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
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:**
- **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...">`
@@ -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).
- **Transformações:**
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest).
- C14N padrão.
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#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>`.
```xml
<ds:Signature Id="sig-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<!-- 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:
@@ -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`).
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`).
3. O certificado X.509 público extraído da chave.
1. **Validação criptográfica de SignedInfo:** Confere se o `SignatureValue` foi emitido pela chave pública correspondente.
2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
3. **Validação de integridade do XAdES:** Confere o digest das propriedades qualificadas (`SignedProperties`), rejeitando qualquer adulteração no horário de assinatura (`SigningTime`) ou no certificado declarado.
4. **Inspeção de signatário e datação:** Extração de nome, documento (CPF/CNPJ), autoridade certificadora emissora e datação auditável.
---
## 5. Integração Web e SDK (@cybercore/signer-sdk)
As aplicações web autorizadas podem assinar arquivos XML diretamente via protocolo CCSP/1 com o método `signXmlDocument()`:
```javascript
import { CyberCoreSigner } from '@cybercore/signer-sdk';
const signer = new CyberCoreSigner();
// Assinatura de documento XML ou evento fiscal
const result = await signer.signXmlDocument({
certificateId: cert.id,
xml: xmlString, // texto puro ou xml_base64
profile: 'XADES_BES', // ou 'XMLDSIG'
targetElementTag: 'infNFe', // opcional
targetElementId: 'NFe35261012345678000199550010000001001234567890' // opcional
});
console.log('XML Assinado Retornado:', result.signed_xml);
console.log('Algoritmo Utilizado:', result.algorithm);
```
@@ -36,6 +36,17 @@ public class CyberCoreSignerApplication {
}
public static void main(String[] args) {
// Previne conflitos nativos de UI Automation/Acessibilidade do Glass no Windows 11
if (System.getProperty("javafx.accessibility") == null) {
System.setProperty("javafx.accessibility", "false");
}
if (System.getProperty("glass.accessible.force") == null) {
System.setProperty("glass.accessible.force", "false");
}
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
logger.error("Exceção não tratada na thread '{}': {}", thread.getName(), throwable.getMessage(), throwable);
});
parseArguments(args);
printBanner();
@@ -67,6 +78,9 @@ public class CyberCoreSignerApplication {
try {
logger.info("Iniciando interface gráfica desktop...");
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
Runtime.getRuntime().addShutdownHook(new Thread(() ->
logger.warn("JVM em encerramento (sinal externo, System.exit ou fim do aplicativo)."),
"cybercore-shutdown-diagnostic"));
Application.launch(CyberCoreDesktopApplication.class, args);
} catch (Throwable t) {
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
@@ -131,12 +145,12 @@ public class CyberCoreSignerApplication {
private static void printBanner() {
System.out.println("""
============================================================
____ _ ____
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___|
|___/
____ _ ____ \s
/ ___| _ _ | |__ ___ _ __ / ___|___ _ __ ___ \s
| | | | | | | '_ \\ / _ \\ '__| | | / _ \\| '__/ _ \\ \s
| |___ | |_| | | |_) | __/ | | |__| (_) | | | __/ \s
\\____| \\__, | |_.__/ \\___|_| \\____\\___/|_| \\___| \s
|___/ \s
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>
</appender>
<appender name="MEMORY" class="br.com.cybercore.signer.app.log.InMemoryLogAppender" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="MEMORY" />
</root>
<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,
CpfDisplayMode cpfDisplayMode,
Boolean includeDateTime,
String customExtraInfo
String customExtraInfo,
Double customSealScale
) {
public PdfSignatureOptions {
@@ -51,6 +52,21 @@ public record PdfSignatureOptions(
if (includeDateTime == null) {
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,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
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,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
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,
@@ -143,6 +159,7 @@ public record PdfSignatureOptions(
}
private String customExtraInfo;
private Double customSealScale = 1.0;
public Builder includeDateTime(boolean include) {
this.includeDateTime = include;
@@ -154,6 +171,11 @@ public record PdfSignatureOptions(
return this;
}
public Builder customSealScale(Double scale) {
this.customSealScale = scale;
return this;
}
public Builder customPosition(Float x, Float y, Integer pageNumber) {
this.customX = x;
this.customY = y;
@@ -179,7 +201,8 @@ public record PdfSignatureOptions(
targetPageNumber,
cpfDisplayMode,
includeDateTime,
customExtraInfo
customExtraInfo,
customSealScale
);
}
}
@@ -1,5 +1,6 @@
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.SigningKeyMaterial;
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.PDPage;
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.PDStream;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
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.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.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
@@ -66,6 +76,8 @@ public class PdfSigningService {
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
}
public record PageDimensions(float width, float height) {}
/**
* Retorna a quantidade total de páginas contidas no documento PDF.
*/
@@ -76,6 +88,21 @@ public class PdfSigningService {
}
}
/**
* Retorna as dimensões em pontos (pt) da página informada do documento PDF.
*/
public static PageDimensions getPageDimensions(byte[] pdfBytes, int pageIndex0Based) throws IOException {
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
}
PDPage page = doc.getPage(pageIndex0Based);
PDRectangle mediaBox = page.getMediaBox();
return new PageDimensions(mediaBox.getWidth(), mediaBox.getHeight());
}
}
/**
* Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada.
*/
@@ -155,6 +182,10 @@ public class PdfSigningService {
String issuerCn = extractIssuerCn(cert.issuerDn());
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");
byte[] pdfDigest = sha256.digest(pdfBytes);
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
@@ -166,7 +197,7 @@ public class PdfSigningService {
// 3. Monta dados do selo visual
PdfSealData sealData = new PdfSealData(
cert.commonName() != null ? cert.commonName() : cert.alias(),
signerName,
signerDoc,
signingTimeStr,
issuerCn,
@@ -178,92 +209,78 @@ public class PdfSigningService {
effectiveExtraInfo
);
// 4. Carrega o PDF e estampa o selo se solicitado
byte[] intermediatePdf;
if (options.visibleSeal()) {
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
} else {
intermediatePdf = pdfBytes;
}
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
// 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
// preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
// o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
// das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(cert.commonName());
signature.setName(signerName);
signature.setLocation(options.location());
signature.setReason(options.reason());
signature.setSignDate(Calendar.getInstance());
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.saveIncremental(finalOut);
}
}
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
return finalOut.toByteArray();
}
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
PdfSignatureOptions options) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
int totalPages = doc.getNumberOfPages();
if (totalPages == 0) {
return pdfBytes;
if (totalPages <= 0) {
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) {
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) {
endPage = 0;
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
startPage = totalPages - 1;
return 0;
}
return totalPages - 1;
}
for (int i = startPage; i <= endPage; i++) {
PDPage page = doc.getPage(i);
private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
PDPage page = doc.getPage(pageIndex);
PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth();
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 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()) {
case BOTTOM_LEFT -> {
x = margin;
@@ -282,15 +299,58 @@ public class PdfSigningService {
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);
return new ByteArrayInputStream(baos.toByteArray());
}
return baos.toByteArray();
}
private static String bytesToHex(byte[] bytes) {
@@ -112,8 +112,10 @@ public final class PdfSealRenderer {
String headerText = resolveHeaderText(theme);
String footerText = resolveFooterText(theme, null);
Color footerColor = new Color(100, 116, 139); // Cinza corporativo neutro e sóbrio (#64748b)
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.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9));
g2d.drawString(headerText, textX, 16);
@@ -127,9 +129,8 @@ public final class PdfSealRenderer {
g2d.drawString("Data: " + data.signingDate(), textX, 60);
g2d.setColor(secondaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8));
g2d.drawString(footerText, textX, 73);
g2d.setColor(footerColor);
drawFittedText(g2d, footerText, textX, 73, textWidthAvailable, 7.8f);
} else if (hasIdentity || hasDate) {
// 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());
drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f);
g2d.setColor(secondaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8));
g2d.drawString(footerText, textX, 71);
g2d.setColor(footerColor);
drawFittedText(g2d, footerText, textX, 71, textWidthAvailable, 7.8f);
} else {
// Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé
@@ -158,9 +158,8 @@ public final class PdfSealRenderer {
g2d.setColor(textColor);
drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f);
g2d.setColor(secondaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 9));
g2d.drawString(footerText, textX, 68);
g2d.setColor(footerColor);
drawFittedText(g2d, footerText, textX, 68, textWidthAvailable, 8.0f);
}
} 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.
*/
private static String formatSignerDoc(PdfSealData data) {
CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL;
String rawDoc = data.identifier() != null ? data.identifier() : "";
String rawDoc = data.identifier() != null ? data.identifier().trim() : "";
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) {
// Se for CPF, oculta. Se for CNPJ, pode manter ou formatar.
if (rawDoc.toUpperCase().contains("CPF")) {
return "";
}
return rawDoc;
}
if (rawDoc.toUpperCase().contains("CPF")) {
String digits = rawDoc.replaceAll("\\D", "");
if (digits.length() == 11) {
String formatted = mode.formatCpf(digits);
return formatted.isBlank() ? "" : "CPF: " + formatted;
}
@@ -378,10 +386,10 @@ public final class PdfSealRenderer {
return theme.customHeader().toUpperCase();
}
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 "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001";
default -> "ASSINADO DIGITALMENTE • ICP-BRASIL";
case "gov-br" -> "DOCUMENTO ASSINADO DIGITALMENTE";
default -> "DOCUMENTO ASSINADO DIGITALMENTE";
};
}
@@ -393,10 +401,10 @@ public final class PdfSealRenderer {
return "Hash Criptográfico: " + hash;
}
return switch (theme.id().toLowerCase()) {
case "cybercore-modern" -> "Autenticidade assegurada por chave criptográfica local";
case "minimal-clean" -> "Assinatura qualificada padrão ICP-Brasil";
case "gov-br" -> "Validade jurídica plena conforme MP nº 2.200-2/2001";
default -> "Conformidade ICP-Brasil e validade jurídica plena";
case "cybercore-modern" -> "Verifique a autenticidade em https://validar.iti.gov.br";
case "minimal-clean" -> "Verifique a autenticidade em https://validar.iti.gov.br";
case "gov-br" -> "Verifique a autenticidade em https://validar.iti.gov.br";
default -> "Verifique a autenticidade em https://validar.iti.gov.br";
};
}
@@ -35,10 +35,10 @@ public final class PdfSealThemeRegistry {
}
private static void registerBuiltInThemes() {
// 1. Tema ICP-Brasil Institucional
// Único Selo Padrão Embutido do Sistema: ICP-Brasil Institucional Oficial
registerTheme(new PdfSealTheme(
"icp-brasil",
"ICP-Brasil Institucional",
DEFAULT_THEME_ID,
"ICP-Brasil Institucional (Padrão)",
"Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
320,
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">
<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}"/>
<!-- Escudo Criptográfico Vetorial -->
<!-- Escudo Criptográfico Vetorial ICP-Brasil -->
<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="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="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="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>
</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>
<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>
"""
));
@@ -205,10 +108,9 @@ public final class PdfSealThemeRegistry {
if (themeId == null) return false;
String idLower = themeId.toLowerCase();
// Protege temas padrão
if (idLower.equals("icp-brasil") || idLower.equals("cybercore-modern") ||
idLower.equals("minimal-clean") || idLower.equals("gov-br")) {
throw new IllegalArgumentException("Não é permitido excluir os temas de selo pré-instalados do sistema.");
// Protege o tema padrão
if (idLower.equals(DEFAULT_THEME_ID)) {
throw new IllegalArgumentException("Não é permitido excluir o selo padrão pré-instalado do sistema.");
}
boolean deleted = STORAGE.deleteSeal(idLower);
@@ -218,7 +120,11 @@ public final class PdfSealThemeRegistry {
public static Optional<PdfSealTheme> findById(String id) {
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) {
@@ -56,7 +56,67 @@ public record CcspRequest(
String purpose,
@JsonProperty("challenge")
String challenge
String challenge,
@JsonProperty("pdf")
String pdf,
@JsonProperty("page")
Integer page,
@JsonProperty("page_choice")
String pageChoice,
@JsonProperty("position")
String position,
@JsonProperty("x")
Float x,
@JsonProperty("y")
Float y,
@JsonProperty("scale")
Double scale,
@JsonProperty("visible_seal")
Boolean visibleSeal,
@JsonProperty("seal_theme_id")
String sealThemeId,
@JsonProperty("seal_custom")
java.util.Map<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 {
public static final String PROTOCOL_VERSION = "1";
@@ -68,7 +128,9 @@ public record CcspRequest(
) {
this(version, requestId, applicationId, operation, algorithm, digest,
certificateHint, expiresAt, timestamp, nonce, signatureType,
null, null, null, null);
null, null, null, null,
null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null);
}
public boolean isVersionSupported() {
@@ -49,7 +49,16 @@ public record CcspResponse(
String documentName,
@JsonProperty("document_type")
String documentType
String documentType,
@JsonProperty("signed_pdf")
String signedPdf,
@JsonProperty("signed_xml")
String signedXml,
@JsonProperty("sha256")
String sha256
) implements Serializable {
public CcspResponse {
@@ -88,7 +97,58 @@ public record CcspResponse(
subjectName,
subjectCpfCnpj,
documentName,
documentType
documentType,
null,
null,
null
);
}
public static CcspResponse pdfSuccess(String requestId, String signedPdfBase64, String sha256,
String signatureBase64, String certBase64, List<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,
subjectCpfCnpj,
null,
"AUTH_CHALLENGE"
"AUTH_CHALLENGE",
null,
null,
null
);
}
@@ -126,6 +189,9 @@ public record CcspResponse(
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.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.domain.SigningResult;
import java.security.MessageDigest;
import br.com.cybercore.signer.core.provider.CertificateManager;
@@ -22,6 +23,20 @@ import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import br.com.cybercore.signer.core.pdf.PdfSealPageChoice;
import br.com.cybercore.signer.core.pdf.PdfSealPosition;
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
import br.com.cybercore.signer.core.pdf.PdfSigningService;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
import br.com.cybercore.signer.core.xml.XmlSignatureProfile;
import br.com.cybercore.signer.core.xml.XmlSigningService;
import java.util.Arrays;
import java.util.UUID;
import java.time.format.DateTimeFormatter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -56,6 +71,8 @@ public class CcspServer {
private final int port;
private final CertificateManager certificateManager;
private final SigningService signingService;
private final PdfSigningService pdfSigningService;
private final XmlSigningService xmlSigningService;
private final ApplicationRegistry applicationRegistry;
private final NonceManager nonceManager;
private final OriginValidator originValidator;
@@ -75,6 +92,8 @@ public class CcspServer {
this.port = port;
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null");
this.pdfSigningService = new PdfSigningService(this.certificateManager);
this.xmlSigningService = new XmlSigningService(this.certificateManager);
this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
this.promptListener = promptListener;
this.nonceManager = new NonceManager();
@@ -114,6 +133,9 @@ public class CcspServer {
httpServer.createContext("/ccsp/v1/sign", new SignHandler());
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler());
httpServer.createContext("/ccsp/v1/sign-pdf", new SignPdfHandler());
httpServer.createContext("/ccsp/v1/sign-xml", new SignXmlHandler());
httpServer.createContext("/ccsp/v1/seals", new SealsHandler());
httpServer.start();
running = true;
@@ -196,8 +218,13 @@ public class CcspServer {
appId = getQueryParam(uri, "application_id");
}
// Verifica autorização se app_id e origin forem informados
if (appId != null && !appId.isBlank()) {
// Exige obrigatoriamente a identificação da aplicação para listagem de certificados
if (appId == null || appId.isBlank()) {
sendErrorResponse(exchange, 401, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
"application_id (via query param ou cabeçalho X-CyberCore-AppId) é obrigatório.");
return;
}
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
if (!authorized) {
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
@@ -207,7 +234,6 @@ public class CcspServer {
return;
}
}
}
List<ManagedCertificate> certs = certificateManager.getAvailableCertificates();
if (certs.isEmpty()) {
@@ -306,10 +332,14 @@ public class CcspServer {
try (InputStream is = exchange.getRequestBody()) {
request = objectMapper.readValue(is, CcspRequest.class);
} 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.");
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
if (!request.isVersionSupported()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
@@ -324,15 +354,18 @@ public class CcspServer {
return;
}
// 3. Validação anti-replay (Nonce)
if (request.nonce() != null && !request.nonce().isBlank()) {
// 3. Validação anti-replay (Nonce obrigatório)
if (request.nonce() == null || request.nonce().isBlank()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
return;
}
boolean accepted = nonceManager.registerNonce(request.nonce());
if (!accepted) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
"Requisição duplicada detectada (replay prevenido).");
return;
}
}
// 4. Validação de autorização da aplicação
String appId = request.applicationId();
@@ -458,6 +491,533 @@ 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;
}
// Validação anti-replay (Nonce obrigatório)
if (request.nonce() == null || request.nonce().isBlank()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
return;
}
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;
}
// Validação anti-replay (Nonce obrigatório)
if (request.nonce() == null || request.nonce().isBlank()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
return;
}
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
// =========================================================================
@@ -487,16 +1047,10 @@ public class CcspServer {
}
private boolean handleCors(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getPath();
String origin = getOrigin(exchange);
if (origin == null || origin.isBlank()) {
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-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce");
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
exchange.close();
@@ -505,6 +1059,41 @@ public class CcspServer {
return false;
}
// Determina se a origem ou rota é elegível para CORS
boolean isHandshakePath = path.endsWith("/status") || path.endsWith("/authorize");
boolean isAuthorizedOrigin = applicationRegistry.findByOrigin(origin).isPresent() || isLocalOrigin(origin);
if (isHandshakePath || isAuthorizedOrigin) {
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
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, Accept");
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
}
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
if (isHandshakePath || isAuthorizedOrigin) {
exchange.sendResponseHeaders(204, -1);
} else {
exchange.sendResponseHeaders(403, -1);
}
exchange.close();
return true;
}
return false;
}
private boolean isLocalOrigin(String origin) {
if (origin == null || origin.isBlank()) return false;
try {
URI uri = URI.create(origin);
String host = uri.getHost();
return "localhost".equalsIgnoreCase(host) || "127.0.0.1".equals(host);
} catch (Exception e) {
return false;
}
}
private String getOrigin(HttpExchange exchange) {
String origin = exchange.getRequestHeaders().getFirst("Origin");
if (origin == null || origin.isBlank()) {
@@ -531,6 +1120,22 @@ public class CcspServer {
try (OutputStream os = exchange.getResponseBody()) {
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 {
@@ -33,12 +33,27 @@ public class CertificateManager {
/**
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
* Suporta chaves de diagnóstico para isolamento de problemas de middleware:
* -Dcybercore.diag.no-wincert=true (não inicializa Windows Certificate Store)
* -Dcybercore.diag.no-pkcs11=true (não inicializa PKCS#11 / DLLs nativas)
*/
public static CertificateManager createDefault() {
CertificateManager manager = new CertificateManager();
if (Boolean.getBoolean("cybercore.diag.no-wincert")) {
logger.warn("Diagnóstico: Provedor Windows Certificate Store DESATIVADO via -Dcybercore.diag.no-wincert=true");
} else {
manager.registerProvider(new WindowsCertificateStoreProvider());
}
manager.registerProvider(new Pkcs12CertificateProvider());
if (Boolean.getBoolean("cybercore.diag.no-pkcs11")) {
logger.warn("Diagnóstico: Provedor PKCS#11 DESATIVADO via -Dcybercore.diag.no-pkcs11=true");
} else {
manager.registerProvider(new Pkcs11CertificateProvider());
}
return manager;
}
@@ -3,6 +3,8 @@ package br.com.cybercore.signer.core.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
@@ -12,15 +14,19 @@ import java.util.concurrent.ConcurrentHashMap;
* Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer.
* Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3
* exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão".
* Os PINs são escopados por aplicação web (applicationId + certId) e possuem expiração por inatividade (TTL de 15 min).
* Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte.
*/
public class PinSessionManager {
private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class);
private static final long SESSION_TTL_MINUTES = 15;
private static final PinSessionManager INSTANCE = new PinSessionManager();
private final Map<String, char[]> sessionPins = new ConcurrentHashMap<>();
private record PinEntry(char[] pin, Instant lastAccessedAt) {}
private final Map<String, PinEntry> sessionPins = new ConcurrentHashMap<>();
private volatile boolean alwaysPromptPin = false;
public PinSessionManager() {
@@ -30,63 +36,107 @@ public class PinSessionManager {
return INSTANCE;
}
private String buildKey(String applicationId, String certId) {
String appId = (applicationId != null && !applicationId.isBlank()) ? applicationId.trim() : "default";
return appId + "::" + certId.trim();
}
/**
* Salva o PIN de um certificado/token para a sessão atual em memória volátil.
* Salva o PIN associado a uma aplicação específica e certificado.
*/
public synchronized void savePin(String certId, char[] pin) {
public synchronized void savePin(String applicationId, String certId, char[] pin) {
Objects.requireNonNull(certId, "certId must not be null");
String key = buildKey(applicationId, certId);
if (pin == null || pin.length == 0) {
clearPin(certId);
clearPin(applicationId, certId);
return;
}
// Limpa PIN anterior caso exista
clearPin(certId);
sessionPins.put(certId, pin.clone());
logger.debug("PIN salvo em memória de sessão para o certificado '{}'. Tamanho: {} dígitos", certId, pin.length);
clearPin(applicationId, certId);
sessionPins.put(key, new PinEntry(pin.clone(), Instant.now()));
logger.debug("PIN salvo em sessão para app '{}' e cert '{}'. Tamanho: {} dígitos", applicationId, certId, pin.length);
}
/**
* Recupera o PIN salvo em sessão para o certificado informado.
* Salva o PIN no escopo padrão (compatibilidade).
*/
public synchronized char[] getPin(String certId) {
public synchronized void savePin(String certId, char[] pin) {
savePin("default", certId, pin);
}
/**
* Recupera o PIN salvo em sessão para a aplicação e certificado especificados.
*/
public synchronized char[] getPin(String applicationId, String certId) {
if (certId == null || alwaysPromptPin) {
return null;
}
char[] stored = sessionPins.get(certId);
return stored != null ? stored.clone() : null;
String key = buildKey(applicationId, certId);
PinEntry entry = sessionPins.get(key);
if (entry == null) {
return null;
}
// Verifica expiração por inatividade (TTL)
if (Instant.now().isAfter(entry.lastAccessedAt().plus(SESSION_TTL_MINUTES, ChronoUnit.MINUTES))) {
clearPin(applicationId, certId);
logger.info("PIN da sessão expirou por inatividade (TTL {} min) para app '{}' e cert '{}'.", SESSION_TTL_MINUTES, applicationId, certId);
return null;
}
// Atualiza timestamp de último acesso
sessionPins.put(key, new PinEntry(entry.pin(), Instant.now()));
return entry.pin().clone();
}
/**
* Verifica se existe um PIN válido salvo nesta sessão para o certificado.
* Recupera o PIN no escopo padrão (compatibilidade).
*/
public synchronized char[] getPin(String certId) {
return getPin("default", certId);
}
/**
* Verifica se existe um PIN válido salvo nesta sessão para a aplicação e certificado.
*/
public synchronized boolean hasPin(String applicationId, String certId) {
return getPin(applicationId, certId) != null;
}
/**
* Verifica se existe um PIN no escopo padrão (compatibilidade).
*/
public synchronized boolean hasPin(String certId) {
if (certId == null || alwaysPromptPin) {
return false;
}
return sessionPins.containsKey(certId);
return hasPin("default", certId);
}
/**
* Remove o PIN salvo para um certificado específico, zerando a memória.
* Remove o PIN salvo para uma aplicação e certificado específicos, zerando a memória.
*/
public synchronized void clearPin(String applicationId, String certId) {
if (certId != null) {
String key = buildKey(applicationId, certId);
PinEntry old = sessionPins.remove(key);
if (old != null && old.pin() != null) {
Arrays.fill(old.pin(), ' ');
}
}
}
/**
* Remove o PIN no escopo padrão (compatibilidade).
*/
public synchronized void clearPin(String certId) {
if (certId != null) {
char[] old = sessionPins.remove(certId);
if (old != null) {
Arrays.fill(old, ' ');
}
}
clearPin("default", certId);
}
/**
* Limpa imediatamente todos os PINs em memória e zera seus caracteres.
*/
public synchronized void clearAll() {
for (char[] pin : sessionPins.values()) {
if (pin != null) {
Arrays.fill(pin, ' ');
for (PinEntry entry : sessionPins.values()) {
if (entry != null && entry.pin() != null) {
Arrays.fill(entry.pin(), ' ');
}
}
sessionPins.clear();
@@ -0,0 +1,308 @@
package br.com.cybercore.signer.core.security;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
* <p>
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
* a partir de semente aleatória protegida no disco local do usuário e atributos estáveis da máquina (PBKDF2WithHmacSHA256).
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
*/
public final class SecurePinStorage {
private static final Logger logger = LoggerFactory.getLogger(SecurePinStorage.class);
private static final String FILE_NAME = "secure-pins.dat";
private static final String SALT_FILE_NAME = "secure-pins.salt";
private static final String SEED_FILE_NAME = "secure-pins.seed";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
private static final int PBKDF2_ITERATIONS = 65536;
private static final int KEY_LENGTH_BITS = 256;
private static final SecurePinStorage INSTANCE = new SecurePinStorage();
private final Path storageDirectory;
private final Path dataFile;
private final Path saltFile;
private final Path seedFile;
private final ObjectMapper objectMapper;
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
public static SecurePinStorage getInstance() {
return INSTANCE;
}
public SecurePinStorage() {
this(resolveDefaultDirectory());
}
public SecurePinStorage(Path storageDirectory) {
this.storageDirectory = Objects.requireNonNull(storageDirectory);
this.dataFile = storageDirectory.resolve(FILE_NAME);
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
this.seedFile = storageDirectory.resolve(SEED_FILE_NAME);
this.objectMapper = new ObjectMapper();
loadFromDisk();
}
private static Path resolveDefaultDirectory() {
String userHome = System.getProperty("user.home", ".");
return Path.of(userHome, ".cybercore");
}
/**
* Armazena de forma persistente e criptografada o PIN associado ao ID do certificado.
*/
public synchronized void savePin(String certId, char[] pin) {
Objects.requireNonNull(certId, "certId não pode ser nulo");
if (pin == null || pin.length == 0) {
removePin(certId);
return;
}
memoryCache.put(certId, pin.clone());
PinSessionManager.getInstance().savePin(certId, pin);
persistToDisk();
logger.info("PIN do certificado '{}' armazenado com segurança neste computador.", certId);
}
/**
* Retorna o PIN armazenado para o certificado ou {@code null} se não houver.
*/
public synchronized char[] getPin(String certId) {
if (certId == null) {
return null;
}
// 1. Tenta cache em memória
char[] cached = memoryCache.get(certId);
if (cached != null) {
return cached.clone();
}
// 2. Tenta carregar do PinSessionManager
char[] sessionPin = PinSessionManager.getInstance().getPin(certId);
if (sessionPin != null) {
memoryCache.put(certId, sessionPin.clone());
return sessionPin.clone();
}
return null;
}
/**
* Verifica se existe um PIN armazenado para o certificado.
*/
public synchronized boolean hasPin(String certId) {
if (certId == null) {
return false;
}
return memoryCache.containsKey(certId) || PinSessionManager.getInstance().hasPin(certId);
}
/**
* Remove o PIN salvo para o certificado e zera a memória.
*/
public synchronized void removePin(String certId) {
if (certId == null) {
return;
}
char[] old = memoryCache.remove(certId);
if (old != null) {
Arrays.fill(old, ' ');
}
PinSessionManager.getInstance().clearPin(certId);
persistToDisk();
logger.info("PIN do certificado '{}' removido do armazenamento seguro.", certId);
}
/**
* Remove todos os PINs armazenados em disco e zera todas as memórias.
*/
public synchronized void clearAll() {
for (char[] pin : memoryCache.values()) {
if (pin != null) {
Arrays.fill(pin, ' ');
}
}
memoryCache.clear();
PinSessionManager.getInstance().clearAll();
try {
Files.deleteIfExists(dataFile);
logger.info("Arquivo de credenciais criptografadas removido com sucesso.");
} catch (Exception e) {
logger.warn("Falha ao remover arquivo de credenciais: {}", e.getMessage());
}
}
public synchronized int getStoredCount() {
return memoryCache.size();
}
// =========================================================================
// CRIPTOGRAFIA AES-256-GCM E DERIVAÇÃO DE CHAVE
// =========================================================================
private void persistToDisk() {
try {
Files.createDirectories(storageDirectory);
if (memoryCache.isEmpty()) {
Files.deleteIfExists(dataFile);
return;
}
Map<String, String> plainMap = new HashMap<>();
for (Map.Entry<String, char[]> entry : memoryCache.entrySet()) {
plainMap.put(entry.getKey(), new String(entry.getValue()));
}
byte[] jsonBytes = objectMapper.writeValueAsBytes(plainMap);
byte[] salt = getOrCreateSalt();
SecretKey secretKey = deriveKey(salt);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] ciphertext = cipher.doFinal(jsonBytes);
// Grava: [12 bytes IV] + [ciphertext + tag]
try (FileOutputStream fos = new FileOutputStream(dataFile.toFile())) {
fos.write(iv);
fos.write(ciphertext);
}
restrictFilePermissions(dataFile);
} catch (Exception e) {
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
}
}
private void loadFromDisk() {
if (!Files.exists(dataFile) || !Files.exists(saltFile)) {
return;
}
try {
byte[] allBytes = Files.readAllBytes(dataFile);
if (allBytes.length <= GCM_IV_LENGTH) {
return;
}
byte[] iv = Arrays.copyOfRange(allBytes, 0, GCM_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(allBytes, GCM_IV_LENGTH, allBytes.length);
byte[] salt = Files.readAllBytes(saltFile);
SecretKey secretKey = deriveKey(salt);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] plainBytes = cipher.doFinal(ciphertext);
Map<String, String> plainMap = objectMapper.readValue(plainBytes, new TypeReference<>() {});
memoryCache.clear();
for (Map.Entry<String, String> entry : plainMap.entrySet()) {
char[] pin = entry.getValue().toCharArray();
memoryCache.put(entry.getKey(), pin);
PinSessionManager.getInstance().savePin(entry.getKey(), pin);
}
logger.info("Carregados {} PIN(s) do armazenamento seguro local.", memoryCache.size());
} catch (Exception e) {
logger.warn("Não foi possível carregar credenciais salvas em disco: {}", e.getMessage());
}
}
private byte[] getOrCreateSalt() throws Exception {
if (Files.exists(saltFile)) {
return Files.readAllBytes(saltFile);
}
byte[] salt = new byte[32];
new SecureRandom().nextBytes(salt);
Files.write(saltFile, salt);
restrictFilePermissions(saltFile);
return salt;
}
private byte[] getOrCreateSeed() throws Exception {
if (Files.exists(seedFile)) {
return Files.readAllBytes(seedFile);
}
byte[] seed = new byte[32];
new SecureRandom().nextBytes(seed);
Files.write(seedFile, seed);
restrictFilePermissions(seedFile);
return seed;
}
private void restrictFilePermissions(Path path) {
try {
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
if (view != null) {
Files.setPosixFilePermissions(path, Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE
));
}
} catch (Exception ignored) {
// Em sistemas não-POSIX como Windows, as permissões são herdadas da pasta de perfil do usuário (~/.cybercore)
}
}
private SecretKey deriveKey(byte[] salt) throws Exception {
byte[] seed = getOrCreateSeed();
String hostIdentity = String.join(":",
System.getProperty("user.name", "unknown"),
System.getProperty("user.home", "unknown"),
System.getProperty("os.name", "unknown"),
System.getProperty("os.arch", "unknown")
);
byte[] hostBytes = hostIdentity.getBytes(StandardCharsets.UTF_8);
byte[] combinedMaterial = new byte[seed.length + hostBytes.length];
System.arraycopy(seed, 0, combinedMaterial, 0, seed.length);
System.arraycopy(hostBytes, 0, combinedMaterial, seed.length, hostBytes.length);
char[] materialChars = Base64.getEncoder().encodeToString(combinedMaterial).toCharArray();
KeySpec spec = new PBEKeySpec(materialChars, salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
Arrays.fill(materialChars, ' ');
Arrays.fill(combinedMaterial, (byte) 0);
return new SecretKeySpec(keyBytes, "AES");
}
}
@@ -15,6 +15,7 @@ import org.bouncycastle.util.Store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
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) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
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));
// Registro de atributos Id no DOM
registerAllIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) {
return new ValidationResult(
ValidationStatus.INVALID,
ValidationType.XML_DSIG,
"Nenhum elemento <Signature> XMLDSig encontrado no documento.",
"Nenhum elemento <Signature> XML encontrado no documento.",
List.of(),
null,
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");
List<ValidationSignerInfo> signers = new ArrayList<>();
boolean allValid = true;
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);
boolean coreValidity = signature.validate(valContext);
@@ -254,22 +282,66 @@ public final class SignatureValidationService {
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(
"Signatário XML #" + (i + 1),
"",
"Assinatura Enveloped XML",
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()),
signerName,
identifier,
issuer,
dateStr,
coreValidity,
coreValidity,
signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
"XMLDSig Element",
"Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA")
certSubject,
(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;
String msg = allValid ? "Documento XML assinado e válido conforme padrão W3C XML-DSig." : "Assinatura XML inválida ou com integridade violada.";
return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName);
String msg = allValid
? (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) {
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.
*/
@@ -7,6 +7,7 @@ public enum ValidationType {
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", 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),
UNKNOWN("Formato Desconhecido", false);
@@ -3,30 +3,43 @@ package br.com.cybercore.signer.core.xml;
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(
String certificateId,
char[] pinOrPassword,
String targetElementTag,
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 XmlSignatureProfile DEFAULT_PROFILE = XmlSignatureProfile.XADES_BES;
public XmlSignatureOptions {
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
signatureAlgorithm = DEFAULT_ALGORITHM;
}
if (profile == null) {
profile = DEFAULT_PROFILE;
}
}
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) {
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.KeySelectorResult;
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.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
@@ -43,25 +45,41 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.Key;
import java.security.MessageDigest;
import java.security.PublicKey;
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.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
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.
* Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.).
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo os padrões:
* - 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).
*/
public class XmlSigningService {
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;
public XmlSigningService(CertificateManager certificateManager) {
@@ -85,7 +103,7 @@ public class XmlSigningService {
try (FileOutputStream fos = new FileOutputStream(targetXml)) {
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();
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)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
@@ -134,13 +153,13 @@ public class XmlSigningService {
// 3. Configuração do XMLSignatureFactory
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
// Transformações padrão: Enveloped + C14N
List<Transform> transforms = new ArrayList<>();
transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
// Transformações padrão para os dados de negócio: Enveloped + C14N
List<Transform> dataTransforms = new ArrayList<>();
dataTransforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
dataTransforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
// Determinação da Referência e Ponto de Inserção
Reference ref;
// Determinação da Referência de Dados e Ponto de Inserção da Assinatura
Reference dataRef;
Node insertionParent;
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.");
}
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();
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
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")) {
String idVal = elem.getAttribute("Id");
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();
} 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();
}
} else {
// 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();
}
// 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(
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) 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();
List<Object> x509Content = new ArrayList<>();
x509Content.add(keyMaterial.signingCertificate());
X509Data x509Data = kif.newX509Data(x509Content);
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);
if (keyMaterial.jcaProvider() != null) {
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);
// 7. Serialização do XML Assinado
// 8. Serialização do XML Assinado
TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
@@ -205,12 +269,13 @@ public class XmlSigningService {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
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();
}
/**
* 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 {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
@@ -218,24 +283,17 @@ public class XmlSigningService {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
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);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
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
NodeList allElements = doc.getElementsByTagName("*");
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);
}
}
}
// Registra nós com atributo Id/id/ID para que o ResourceResolver localize as referências
registerIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
@@ -248,8 +306,168 @@ public class XmlSigningService {
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
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);
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) {
@@ -264,6 +482,17 @@ public class XmlSigningService {
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.
*/
@@ -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.operator.ContentSigner;
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.DisplayName;
import org.junit.jupiter.api.Test;
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
private CertificateManager certificateManager;
private PdfSigningService pdfSigningService;
private String testCertId;
private String testCert2Id;
@BeforeEach
void setUp() throws Exception {
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
char[] pwd = "password123".toCharArray();
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");
provider.setKeyStore(ks, pwd);
certificateManager.registerProvider(provider);
List<ManagedCertificate> list = certificateManager.discoverCertificates();
assertThat(list).isNotEmpty();
this.testCertId = list.get(0).id();
assertThat(list).hasSize(2);
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);
}
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
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.security.ApplicationRegistry;
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.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -49,6 +70,28 @@ class CcspServerTest {
SigningService signingService = mock(SigningService.class);
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
appRegistry.register(new AuthorizedApplication(
"app-test",
@@ -64,8 +107,8 @@ class CcspServerTest {
"AliasTeste",
CertificateType.A1,
CertificateStatus.AVAILABLE,
"CN=Teste ICP-Brasil, C=BR",
"CN=AC Raiz, C=BR",
"CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR",
"CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR",
"12345678",
"a1b2c3",
"d4e5f6",
@@ -89,6 +132,9 @@ class CcspServerTest {
@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());
}
@Override public Optional<SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) {
return Optional.of(new SigningKeyMaterial(kp.getPrivate(), new X509Certificate[]{cert}, null));
}
};
certManager.registerProvider(dummyProvider);
@@ -149,7 +195,64 @@ class CcspServerTest {
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"success\"");
assertThat(resp.body()).contains("CN=Teste ICP-Brasil, C=BR");
assertThat(resp.body()).contains("Teste ICP-Brasil");
}
@Test
@DisplayName("Endpoint /certificates deve rejeitar com 401 requisições sem application_id")
void testGetCertificatesShouldRejectWhenAppIdMissing() throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/certificates"))
.header("Origin", "https://meusistema.com.br")
.GET()
.build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(401);
assertThat(resp.body()).contains("APPLICATION_NOT_AUTHORIZED");
}
@Test
@DisplayName("Requisição OPTIONS com origem desconhecida deve ser rejeitada com 403")
void testCorsPreflightShouldRejectUnauthorizedOrigin() throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
.method("OPTIONS", HttpRequest.BodyPublishers.noBody())
.header("Origin", "https://malicious-attacker.com")
.build();
HttpResponse<Void> resp = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
assertThat(resp.statusCode()).isEqualTo(403);
}
@Test
@DisplayName("Endpoint /sign deve rejeitar requisição sem nonce")
void testSignShouldRejectWhenNonceIsMissing() throws Exception {
byte[] docHash = MessageDigest.getInstance("SHA-256").digest("Documento".getBytes());
String hashBase64 = Base64.getEncoder().encodeToString(docHash);
String jsonPayload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-test",
"operation": "sign",
"algorithm": "SHA256withRSA",
"digest": "%s",
"certificate_hint": "cert-1"
}
""".formatted(UUID.randomUUID(), hashBase64);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
.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(400);
assertThat(resp.body()).contains("REPLAY_DETECTED");
}
@Test
@@ -320,4 +423,196 @@ class CcspServerTest {
assertThat(response.body()).contains("\"document_name\":\"proposta_comercial.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("ICP-Brasil Institucional");
}
@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");
}
}
@@ -63,4 +63,34 @@ class CertificateManagerTest {
assertThat(byCnpj).isPresent();
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
}
@Test
@DisplayName("Deve desativar provider PKCS#11 quando chave de diagnóstico estiver ativa")
void shouldRespectDiagnosticsNoPkcs11() {
System.setProperty("cybercore.diag.no-pkcs11", "true");
try {
CertificateManager manager = CertificateManager.createDefault();
assertThat(manager.getProviders())
.extracting(CertificateProvider::getProviderId)
.doesNotContain("provider-pkcs11")
.contains("provider-a1");
} finally {
System.clearProperty("cybercore.diag.no-pkcs11");
}
}
@Test
@DisplayName("Deve desativar provider Windows Certificate Store quando chave de diagnóstico estiver ativa")
void shouldRespectDiagnosticsNoWinCert() {
System.setProperty("cybercore.diag.no-wincert", "true");
try {
CertificateManager manager = CertificateManager.createDefault();
assertThat(manager.getProviders())
.extracting(CertificateProvider::getProviderId)
.doesNotContain("provider-win-store")
.contains("provider-a1");
} finally {
System.clearProperty("cybercore.diag.no-wincert");
}
}
}
@@ -58,4 +58,17 @@ class PinSessionManagerTest {
assertThat(manager.hasPin("cert-a")).isFalse();
assertThat(manager.hasPin("cert-b")).isFalse();
}
@Test
@DisplayName("Deve isolar PINs entre diferentes aplicações para o mesmo certificado")
void shouldIsolatePinByApplicationId() {
manager.savePin("app-1", "cert-x", "pin-app-1".toCharArray());
assertThat(manager.hasPin("app-1", "cert-x")).isTrue();
assertThat(manager.getPin("app-1", "cert-x")).isEqualTo("pin-app-1".toCharArray());
// Aplicação 2 não deve ter acesso ao PIN da Aplicação 1
assertThat(manager.hasPin("app-2", "cert-x")).isFalse();
assertThat(manager.getPin("app-2", "cert-x")).isNull();
}
}
@@ -0,0 +1,77 @@
package br.com.cybercore.signer.core.security;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class SecurePinStorageTest {
@TempDir
Path tempDir;
private SecurePinStorage storage;
@BeforeEach
void setUp() {
storage = new SecurePinStorage(tempDir);
}
@AfterEach
void tearDown() {
storage.clearAll();
}
@Test
void shouldSaveAndRetrievePinCorrectly() {
String certId = "cert-test-123";
char[] pin = "123456".toCharArray();
storage.savePin(certId, pin);
assertThat(storage.hasPin(certId)).isTrue();
assertThat(storage.getPin(certId)).isEqualTo(pin);
}
@Test
void shouldPersistAndReloadFromDisk() {
String certId = "token-a3-999";
char[] pin = "senhaForte987".toCharArray();
storage.savePin(certId, pin);
// Cria nova instância apontando para o mesmo diretório
SecurePinStorage reloaded = new SecurePinStorage(tempDir);
assertThat(reloaded.hasPin(certId)).isTrue();
assertThat(reloaded.getPin(certId)).isEqualTo(pin);
}
@Test
void shouldRemoveSpecificPin() {
storage.savePin("cert-1", "1111".toCharArray());
storage.savePin("cert-2", "2222".toCharArray());
storage.removePin("cert-1");
assertThat(storage.hasPin("cert-1")).isFalse();
assertThat(storage.getPin("cert-1")).isNull();
assertThat(storage.hasPin("cert-2")).isTrue();
}
@Test
void shouldClearAllPins() {
storage.savePin("cert-1", "1111".toCharArray());
storage.savePin("cert-2", "2222".toCharArray());
storage.clearAll();
assertThat(storage.hasPin("cert-1")).isFalse();
assertThat(storage.hasPin("cert-2")).isFalse();
assertThat(storage.getStoredCount()).isEqualTo(0);
}
}
@@ -72,8 +72,8 @@ class XmlSigningServiceTest {
}
@Test
@DisplayName("Deve assinar documento XML completo com XMLDSig Enveloped e validar com sucesso")
void shouldSignWholeXmlDocumentAndValidate() throws Exception {
@DisplayName("Deve assinar documento XML no padrão XAdES-BES por padrão e validar com sucesso")
void shouldSignWholeXmlDocumentWithXadesBesByDefaultAndValidate() throws Exception {
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<pedido id="ped-1001">
@@ -86,19 +86,38 @@ class XmlSigningServiceTest {
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
// Verifica tags essenciais do XMLDSig
assertThat(signedXmlStr).contains("<Signature");
assertThat(signedXmlStr).contains("<SignedInfo");
assertThat(signedXmlStr).contains("<SignatureValue");
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);
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
@DisplayName("Deve assinar nó XML específico com atributo Id (estilo NF-e) e validar")
void shouldSignSpecificXmlElementById() throws Exception {
@DisplayName("Deve assinar nó XML específico com atributo Id no padrão XAdES-BES e validar")
void shouldSignSpecificXmlElementWithXadesBes() throws Exception {
String nfeXml = """
<?xml version="1.0" encoding="UTF-8"?>
<NFe xmlns="http://www.portalfiscal.inf.br/nfe">
@@ -117,21 +136,48 @@ class XmlSigningServiceTest {
testCertId,
null,
"infNFe",
"NFe35261012345678000195550010000000011000000010"
"NFe35261012345678000195550010000000011000000010",
XmlSignatureProfile.XADES_BES
);
byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options);
String signedStr = new String(signedNfe, StandardCharsets.UTF_8);
assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\"");
assertThat(signedStr).contains("QualifyingProperties");
assertThat(signedStr).contains("SignedProperties");
boolean isValid = xmlSigningService.validateXmlSignature(signedNfe);
assertThat(isValid).isTrue();
}
@Test
@DisplayName("Deve detectar adulteração em XML assinado e reprovar na validação")
void shouldDetectTamperingInSignedXml() throws Exception {
@DisplayName("Deve assinar documento XML no perfil XMLDSig clássico quando explicitamente solicitado")
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 = """
<?xml version="1.0" encoding="UTF-8"?>
<contrato>
@@ -142,11 +188,36 @@ class XmlSigningServiceTest {
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
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)
.replace("R$ 100,00", "R$ 999.999,00");
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
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
this.certificateManager = CertificateManager.createDefault();
this.signingService = new SigningService(this.certificateManager);
if (developmentMode) {
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
@@ -104,22 +105,23 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView = new MainAppView(primaryStage);
// Ações da Grade de Assinatura e Validação
mainAppView.setOnSignPdf(() -> new PdfSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignPdf(() -> handleStartPdfSigning(primaryStage));
mainAppView.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show());
mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show());
mainAppView.setOnCoSign(() -> new FileSignDialog(primaryStage, "Co-Assinar Documento", "*.*", SvgIcons.coSignIcon(), certificateManager, signingService).show());
mainAppView.setOnTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show());
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage, false).show());
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage).show());
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
mainAppView.setOnExit(() -> {
logger.info("Encerrando CyberCore Signer pela interface gráfica.");
logger.info("Ação 'Sair' acionada na interface gráfica.");
if (systemTrayManager != null) {
systemTrayManager.handleWindowCloseRequest();
} else {
primaryStage.close();
Platform.exit();
System.exit(0);
}
});
// Ações do Painel Lateral
@@ -129,21 +131,44 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
// Observabilidade do ciclo de vida da janela
primaryStage.showingProperty().addListener((obs, oldVal, newVal) ->
logger.info("Visibilidade da janela principal JavaFX alterada: visível={}", newVal));
primaryStage.iconifiedProperty().addListener((obs, oldVal, newVal) ->
logger.info("Estado de minimização da janela JavaFX alterado: minimizada={}", newVal));
primaryStage.focusedProperty().addListener((obs, oldVal, newVal) ->
logger.info("Foco da janela principal alterado: focada={}", newVal));
primaryStage.xProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal movida: x={}, y={}", newVal, primaryStage.getY()));
primaryStage.yProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal movida: x={}, y={}", primaryStage.getX(), newVal));
primaryStage.widthProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal redimensionada: largura={}, altura={}", newVal, primaryStage.getHeight()));
primaryStage.heightProperty().addListener((obs, oldVal, newVal) ->
logger.debug("Janela principal redimensionada: largura={}, altura={}", primaryStage.getWidth(), newVal));
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
initCcspServer(primaryStage);
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
initSmartCardWatcher();
// Configura bandeja do sistema (System Tray) para operação contínua em segundo plano
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
this.systemTrayManager.setup();
// Carrega os certificados instalados na máquina do usuário
// Carrega os certificados instalados na máquina do usuário de forma estável
refreshCertificates();
// Exibe a janela principal em primeiro plano após os serviços estarem prontos
mainAppView.show();
logger.info("Janela principal JavaFX exibida.");
// Configura a bandeja (System Tray) somente depois que a janela JavaFX está visível,
// para não inicializar o toolkit AWT concorrentemente com a criação da janela.
if (Boolean.getBoolean("cybercore.no.tray")) {
logger.warn("System Tray desativado via -Dcybercore.no.tray=true (diagnóstico).");
} else {
Platform.runLater(() -> this.systemTrayManager.setup());
}
}
private void refreshCertificates() {
@@ -155,6 +180,19 @@ public class CyberCoreDesktopApplication extends Application {
}
}
private void handleStartPdfSigning(Stage ownerStage) {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Documento PDF para Assinar");
chooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"),
new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")
);
File selectedFile = chooser.showOpenDialog(ownerStage);
if (selectedFile != null && selectedFile.exists()) {
new PdfSignDialog(ownerStage, certificateManager, selectedFile).show();
}
}
private void openSettingsDialog(Stage owner) {
Stage dlg = new Stage();
dlg.initModality(Modality.APPLICATION_MODAL);
@@ -178,8 +216,9 @@ public class CyberCoreDesktopApplication extends Application {
VBox.setVgrow(scrollPane, Priority.ALWAYS);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
@@ -206,8 +245,9 @@ public class CyberCoreDesktopApplication extends Application {
AboutView aboutView = new AboutView();
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
@@ -409,6 +449,9 @@ public class CyberCoreDesktopApplication extends Application {
byte[] digestBytes;
if (request.digest() != null && !request.digest().isBlank()) {
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()) {
digestBytes = MessageDigest.getInstance("SHA-256")
.digest(request.challenge().getBytes(StandardCharsets.UTF_8));
@@ -453,12 +496,23 @@ public class CyberCoreDesktopApplication extends Application {
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
this.ccspServer.start();
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
} catch (java.net.BindException be) {
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
} catch (Exception e) {
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
} else {
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
}
}
}
private void initSmartCardWatcher() {
if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) {
logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");
return;
}
try {
this.smartCardWatcher = new SmartCardWatcher();
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
@@ -5,8 +5,14 @@ import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
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.Line;
import javafx.scene.shape.Polygon;
@@ -17,7 +23,6 @@ import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@@ -26,19 +31,20 @@ import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
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.function.Supplier;
/**
* 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/'
* 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) {
if (iconName != null && !iconName.isBlank()) {
@@ -140,7 +146,20 @@ public final class AssetManager {
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);
if (customImg.isPresent() && !customImg.get().isError()) {
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) {
// Tenta carregar app-logo bitmap (.png) primeiro
Optional<Image> customLogo = loadImage("images", "app-logo", width, height);
if (customLogo.isPresent() && !customLogo.get().isError()) {
ImageView iv = new ImageView(customLogo.get());
public static Node getImageOrDefault(String imageName, double width, double height, Supplier<Node> fallbackSupplier) {
if (imageName != null && !imageName.isBlank()) {
String cleanName = cleanBaseName(imageName);
// 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.setFitHeight(height);
iv.setPreserveRatio(true);
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();
}
/**
* 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) {
String clean = name;
int dotIdx = clean.lastIndexOf('.');
@@ -189,7 +256,7 @@ public final class AssetManager {
private static Optional<Node> loadSvgIconFromPath(Path path, double size) {
if (Files.isRegularFile(path)) {
try (InputStream in = new FileInputStream(path.toFile())) {
Node node = parseSvgToNode(in, size);
Node node = parseSvgToNode(in, size, size);
if (node != null) {
logger.debug("Carregado ícone SVG customizado de: {}", path);
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) {
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 {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(false);
@@ -249,27 +323,34 @@ public final class AssetManager {
if (vbWidth <= 0) vbWidth = 48;
if (vbHeight <= 0) vbHeight = 48;
Group contentGroup = new Group();
renderChildren(svg, contentGroup);
Map<String, Paint> defsMap = new HashMap<>();
parseDefs(svg, defsMap);
double maxDim = Math.max(vbWidth, vbHeight);
double scale = (targetSize * 0.90) / maxDim;
Group contentGroup = new Group();
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 drawnH = vbHeight * scale;
double tx = (targetSize - drawnW) / 2.0 - (vbMinX * scale);
double ty = (targetSize - drawnH) / 2.0 - (vbMinY * scale);
double tx = (targetWidth - drawnW) / 2.0 - (vbMinX * scale);
double ty = (targetHeight - drawnH) / 2.0 - (vbMinY * scale);
contentGroup.setManaged(false);
contentGroup.setLayoutX(0);
contentGroup.setLayoutY(0);
contentGroup.getTransforms().setAll(
new javafx.scene.transform.Translate(tx, ty),
new javafx.scene.transform.Scale(scale, scale, 0, 0)
);
StackPane container = new StackPane(contentGroup);
container.setPrefSize(targetSize, targetSize);
container.setMaxSize(targetSize, targetSize);
container.setMinSize(targetSize, targetSize);
container.setAlignment(Pos.CENTER);
Pane container = new Pane(contentGroup);
container.setPrefSize(targetWidth, targetHeight);
container.setMaxSize(targetWidth, targetHeight);
container.setMinSize(targetWidth, targetHeight);
return container;
} 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();
for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node n = children.item(i);
if (n instanceof Element el) {
Node jfxNode = createElementNode(el);
if ("defs".equalsIgnoreCase(el.getTagName())) {
continue;
}
Node jfxNode = createElementNode(el, defsMap);
if (jfxNode != null) {
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();
Shape shape = null;
@@ -350,56 +519,107 @@ public final class AssetManager {
double x = parseDouble(el.getAttribute("x"), 0);
double y = parseDouble(el.getAttribute("y"), 0);
String content = el.getTextContent();
Text t = new Text(x, y, content);
double fs = parseDouble(el.getAttribute("font-size"), 10);
Text t = new Text(content != null ? content.trim() : "");
double fs = parseDouble(el.getAttribute("font-size"), 12);
String fw = el.getAttribute("font-weight");
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
if (el.hasAttribute("fill")) {
t.setFill(parseColor(el.getAttribute("fill"), Color.BLACK));
Map<String, String> inlineStyle = el.hasAttribute("style") ? parseStyleAttribute(el.getAttribute("style")) : Map.of();
String fillVal = inlineStyle.getOrDefault("fill", el.hasAttribute("fill") ? el.getAttribute("fill") : null);
if (fillVal != null) {
t.setFill(parsePaint(fillVal, defsMap, Color.BLACK));
}
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
String anchor = inlineStyle.getOrDefault("text-anchor", el.getAttribute("text-anchor"));
String baseline = inlineStyle.getOrDefault("dominant-baseline", el.getAttribute("dominant-baseline"));
boolean isCenterBaseline = "central".equalsIgnoreCase(baseline) || "middle".equalsIgnoreCase(baseline);
if (isCenterBaseline) {
t.setTextOrigin(javafx.geometry.VPos.CENTER);
} else {
t.setTextOrigin(javafx.geometry.VPos.BASELINE);
}
double textWidth = t.getLayoutBounds().getWidth();
double finalX = x;
if ("middle".equalsIgnoreCase(anchor)) {
finalX = x - (textWidth / 2.0);
} else if ("end".equalsIgnoreCase(anchor)) {
finalX = x - textWidth;
}
t.setX(finalX);
t.setY(y);
return t;
}
case "g" -> {
Group g = new Group();
renderChildren(el, g);
renderChildren(el, g, defsMap);
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);
for (Node child : g.getChildren()) {
if (child instanceof Shape s) {
if (strokeCol != null) s.setStroke(strokeCol);
if (strokePaint != null) s.setStroke(strokePaint);
s.setStrokeWidth(sw);
}
}
}
applyElementTransform(el, g);
return g;
}
}
if (shape != null) {
applyShapeStyles(el, shape);
applyShapeStyles(el, shape, defsMap);
applyElementTransform(el, shape);
return shape;
}
return null;
}
private static void applyShapeStyles(Element el, Shape shape) {
if (el.hasAttribute("fill")) {
shape.setFill(parseColor(el.getAttribute("fill"), Color.TRANSPARENT));
private static Map<String, String> parseStyleAttribute(String styleAttr) {
Map<String, String> styleMap = new HashMap<>();
if (styleAttr == null || styleAttr.isBlank()) {
return styleMap;
}
String[] declarations = styleAttr.split(";");
for (String decl : declarations) {
int colonIdx = decl.indexOf(':');
if (colonIdx > 0) {
String prop = decl.substring(0, colonIdx).trim().toLowerCase();
String val = decl.substring(colonIdx + 1).trim();
styleMap.put(prop, val);
}
}
return styleMap;
}
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
Map<String, String> inlineStyle = el.hasAttribute("style") ? parseStyleAttribute(el.getAttribute("style")) : Map.of();
String fillVal = inlineStyle.getOrDefault("fill", el.hasAttribute("fill") ? el.getAttribute("fill") : null);
if (fillVal != null) {
shape.setFill(parsePaint(fillVal, defsMap, Color.TRANSPARENT));
} else if (shape instanceof Line) {
shape.setFill(Color.TRANSPARENT);
}
if (el.hasAttribute("stroke")) {
shape.setStroke(parseColor(el.getAttribute("stroke"), Color.TRANSPARENT));
String strokeVal = inlineStyle.getOrDefault("stroke", el.hasAttribute("stroke") ? el.getAttribute("stroke") : null);
if (strokeVal != null) {
shape.setStroke(parsePaint(strokeVal, defsMap, Color.TRANSPARENT));
}
if (el.hasAttribute("stroke-width")) {
shape.setStrokeWidth(parseDouble(el.getAttribute("stroke-width"), 1.0));
String strokeWidthVal = inlineStyle.getOrDefault("stroke-width", el.hasAttribute("stroke-width") ? el.getAttribute("stroke-width") : null);
if (strokeWidthVal != null) {
shape.setStrokeWidth(parseDouble(strokeWidthVal, 1.0));
}
String opacityVal = inlineStyle.getOrDefault("opacity", el.hasAttribute("opacity") ? el.getAttribute("opacity") : null);
if (opacityVal != null) {
shape.setOpacity(parseDouble(opacityVal, 1.0));
}
if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) {
@@ -409,6 +629,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) {
String[] tokens = ptsStr.trim().split("[,\\s]+");
for (String t : tokens) {
@@ -446,4 +682,52 @@ public final class AssetManager {
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);
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");
HBox.setHgrow(titleLabel, Priority.ALWAYS);
Label badgeLabel = new Label();
Label badgeLabel = new Label("[" + certificate.type().getCode() + "]");
if (certificate.type() == CertificateType.A1) {
badgeLabel.setText("A1 • Certificado local");
badgeLabel.getStyleClass().add("cert-badge-a1");
} else {
String tokenDesc = certificate.hardwareDeviceName() != null ? certificate.hardwareDeviceName() : "Token USB";
badgeLabel.setText("A3 • " + tokenDesc);
badgeLabel.getStyleClass().add("cert-badge-a3");
}
@@ -709,6 +709,10 @@ public final class SvgIcons {
}
public static Node closeIcon(double size) {
return AssetManager.getIconOrDefault("close", size, () -> buildCloseIcon(size));
}
public static Node buildCloseIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
@@ -729,4 +733,148 @@ public final class SvgIcons {
box.getChildren().add(cross);
return box;
}
/**
* Ícone vetorial de Seta para a Direita / Avançar.
*/
public static Node arrowRightIcon(double size) {
return arrowRightIcon(size, "#ffffff");
}
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();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath arrow = new SVGPath();
arrow.setContent("M 5 3 L 10.5 8 L 5 13");
arrow.setStroke(Color.web(colorHex));
arrow.setStrokeWidth(2.2);
arrow.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
arrow.setStrokeLineJoin(javafx.scene.shape.StrokeLineJoin.ROUND);
arrow.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
arrow.setScaleX(scale);
arrow.setScaleY(scale);
box.getChildren().add(arrow);
return box;
}
/**
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
*/
public static Node zoomInIcon(double size) {
return AssetManager.getIconOrDefault("zoom-in", size, () -> buildZoomInIcon(size));
}
public static Node buildZoomInIcon(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 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5 M 6.5 4.5 L 6.5 8.5");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
public static Node zoomOutIcon(double size) {
return AssetManager.getIconOrDefault("zoom-out", size, () -> buildZoomOutIcon(size));
}
public static Node buildZoomOutIcon(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 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
public static Node zoomFitIcon(double size) {
return AssetManager.getIconOrDefault("zoom-fit", size, () -> buildZoomFitIcon(size));
}
public static Node buildZoomFitIcon(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 2 5 L 2 2 L 5 2 M 11 2 L 14 2 L 14 5 M 14 11 L 14 14 L 11 14 M 5 14 L 2 14 L 2 11");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
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,5 +1,6 @@
package br.com.cybercore.signer.ui.tray;
import br.com.cybercore.signer.ui.view.ExitConfirmationDialog;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.slf4j.Logger;
@@ -40,12 +41,19 @@ public class SystemTrayManager {
/**
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
*/
public synchronized boolean setup() {
public boolean setup() {
if (!SystemTray.isSupported()) {
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
return false;
}
// O TrayIcon AWT é criado na thread de eventos do AWT, nunca na thread do JavaFX,
// para evitar conflito entre os dois loops de mensagens nativos no Windows.
java.awt.EventQueue.invokeLater(this::setupOnAwtThread);
return true;
}
private synchronized void setupOnAwtThread() {
try {
SystemTray tray = SystemTray.getSystemTray();
Image iconImage = createTrayIconImage();
@@ -76,38 +84,78 @@ public class SystemTrayManager {
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
this.trayIcon.setImageAutoSize(true);
// Clique no ícone restaura a janela
this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
// Duplo clique (ActionListener) e clique simples (MouseListener) restauram a janela
this.trayIcon.addActionListener(e -> {
logger.info("Ícone da bandeja acionado (duplo clique).");
Platform.runLater(this::showStage);
});
this.trayIcon.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getButton() == java.awt.event.MouseEvent.BUTTON1) {
logger.info("Ícone da bandeja acionado (clique simples).");
Platform.runLater(SystemTrayManager.this::showStage);
}
}
});
tray.add(this.trayIcon);
this.initialized = true;
// Intercepta fechamento da janela para ocultar em vez de encerrar
// Intercepta fechamento da janela (botão X) para confirmar ação com o usuário
Platform.runLater(() -> {
stage.setOnCloseRequest(event -> {
event.consume();
stage.hide();
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
handleWindowCloseRequest();
});
});
logger.info("System Tray configurado com sucesso.");
return true;
} catch (AWTException e) {
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
return false;
} catch (Throwable t) {
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage());
return false;
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage(), t);
}
}
/**
* Exibe o diálogo de confirmação ao clicar no 'X' da janela principal,
* permitindo minimizar para a bandeja, encerrar definitivamente ou cancelar.
*/
public void handleWindowCloseRequest() {
try {
ExitConfirmationDialog dialog = new ExitConfirmationDialog(stage);
ExitConfirmationDialog.ExitChoice choice = dialog.showAndGetChoice();
switch (choice) {
case MINIMIZE_TO_TRAY -> {
logger.info("Usuário optou por minimizar o CyberCore Signer para a bandeja.");
stage.hide();
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
}
case EXIT_COMPLETELY -> {
logger.info("Usuário optou por encerrar definitivamente o CyberCore Signer.");
shutdown();
}
case CANCEL -> {
logger.debug("Fechamento da janela cancelado pelo usuário.");
}
}
} catch (Throwable t) {
logger.warn("Erro ao exibir diálogo de fechamento, minimizando para bandeja por segurança: {}", t.getMessage());
stage.hide();
}
}
public void showStage() {
if (stage != null) {
logger.info("Restaurando janela principal (visível={}, minimizada={}).",
stage.isShowing(), stage.isIconified());
stage.show();
stage.setIconified(false);
stage.toFront();
stage.requestFocus();
}
}
@@ -1,12 +1,15 @@
package br.com.cybercore.signer.ui.view;
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.scene.Node;
import javafx.scene.control.Label;
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")
public final class AboutView extends VBox {
@@ -16,13 +19,12 @@ public final class AboutView extends VBox {
}
private void initView() {
setSpacing(16);
setSpacing(14);
setAlignment(Pos.CENTER);
getStyleClass().add("content-pane");
Label brandLabel = new Label(CyberCoreUiConstants.APP_NAME);
brandLabel.getStyleClass().add("brand-title");
brandLabel.setStyle("-fx-font-size: 24px;");
// Logotipo oficial vetorial SVG (app-logo.svg) com fallback para cristal 3D
Node logoNode = AssetManager.getLogoOrDefault(320, 80, () -> SvgIcons.createAppCrystalLogo(54));
Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1");
versionLabel.getStyleClass().add("status-badge-active");
@@ -44,9 +46,9 @@ public final class AboutView extends VBox {
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");
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.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);
Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO");
titleLabel.getStyleClass().add("dialog-header");
@@ -48,6 +51,8 @@ public class ApplicationApprovalDialog {
subtitleLabel.getStyleClass().add("dialog-subtitle");
headerBox.getChildren().addAll(titleLabel, subtitleLabel);
header.getChildren().addAll(br.com.cybercore.signer.ui.components.SvgIcons.cyberShieldLogo(34), headerBox);
// Painel de Detalhes da Aplicação
VBox detailsBox = new VBox(10);
detailsBox.getStyleClass().add("dialog-box");
@@ -106,7 +111,7 @@ public class ApplicationApprovalDialog {
actionsRow.getChildren().addAll(btnReject, btnAuthorize);
root.getChildren().addAll(headerBox, detailsBox, secWarning, actionsRow);
root.getChildren().addAll(header, detailsBox, secWarning, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
@@ -386,7 +386,7 @@ public final class BatchSigningDialog {
Path dest = targetDir.resolve(outPdfName);
pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
} 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 =
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
@@ -129,8 +129,9 @@ public class CertificateDetailsDialog {
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
actionsRow.getChildren().add(btnClose);
@@ -77,7 +77,7 @@ public final class CertificatesManagerDialog {
btnAddA1.getStyleClass().add("btn-primary");
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.setOnAction(e -> refreshList());
@@ -93,7 +93,7 @@ public final class CertificatesManagerDialog {
HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button("Fechar");
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
@@ -0,0 +1,111 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
/**
* Diálogo modal de confirmação ao fechar a janela principal do CyberCore Signer.
* Permite ao usuário escolher entre manter o agente ativo na bandeja do sistema
* ou encerrar completamente a aplicação.
*/
public final class ExitConfirmationDialog {
public enum ExitChoice {
MINIMIZE_TO_TRAY,
EXIT_COMPLETELY,
CANCEL
}
private final Stage stage;
private ExitChoice chosenAction = ExitChoice.CANCEL;
public ExitConfirmationDialog(Stage owner) {
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Encerrar ou Manter na Bandeja");
this.stage.setResizable(false);
this.stage.setMinWidth(520);
this.stage.setMinHeight(190);
initLayout();
}
private void initLayout() {
VBox root = new VBox(22);
root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container");
// Cabeçalho com ícone e texto explicativo
HBox header = new HBox(16);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(4);
Label title = new Label("Como você deseja proceder?");
title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;");
Label subtitle = new Label(
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
"para atender assinaturas de aplicações web, ou encerrá-lo por completo."
);
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
titleBox.getChildren().addAll(title, subtitle);
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
// Barra de Ações com os botões
HBox actionsRow = new HBox(10);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog");
btnCancel.setOnAction(e -> {
chosenAction = ExitChoice.CANCEL;
stage.close();
});
HBox spacer = new HBox();
HBox.setHgrow(spacer, Priority.ALWAYS);
Button btnExit = new Button("Encerrar Definitivamente");
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog");
btnExit.setOnAction(e -> {
chosenAction = ExitChoice.EXIT_COMPLETELY;
stage.close();
});
Button btnMinimize = new Button("Minimizar para a Bandeja");
btnMinimize.getStyleClass().addAll("btn-primary", "btn-dialog");
btnMinimize.setDefaultButton(true);
btnMinimize.setOnAction(e -> {
chosenAction = ExitChoice.MINIMIZE_TO_TRAY;
stage.close();
});
actionsRow.getChildren().addAll(btnCancel, spacer, btnExit, btnMinimize);
root.getChildren().addAll(header, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
}
public ExitChoice showAndGetChoice() {
stage.showAndWait();
return chosenAction;
}
}
@@ -57,21 +57,43 @@ public final class LogsViewerDialog {
txtLogs.getStyleClass().add("log-console-area");
VBox.setVgrow(txtLogs, Priority.ALWAYS);
loadInitialLogs();
loadLogs();
HBox footer = new HBox(10);
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");
btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.setOnAction(e -> loadInitialLogs());
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnRefresh.setPrefWidth(100);
btnRefresh.setOnAction(e -> loadLogs());
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(100);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().addAll(btnRefresh, btnClose);
footer.getChildren().addAll(btnCopy, btnClear, btnRefresh, btnClose);
root.getChildren().addAll(header, txtLogs, footer);
@@ -81,15 +103,15 @@ public final class LogsViewerDialog {
stage.setScene(scene);
}
private void loadInitialLogs() {
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String sb = "[" + now + "] [INFO] CyberCore Signer inicializado com sucesso.\n" +
"[" + now + "] [INFO] Servidor local CCSP/1 escutando em http://127.0.0.1:21325\n" +
"[" + now + "] [INFO] Provedores criptográficos registrados: Windows Certificate Store, PKCS#12, PKCS#11.\n" +
"[" + now + "] [INFO] SmartCardWatcher ativo (monitoramento de tokens USB e Smart Cards ICP-Brasil).\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.setText(sb);
private void loadLogs() {
java.util.List<String> logs = br.com.cybercore.signer.core.log.AppLogBuffer.getLogs();
if (logs.isEmpty()) {
txtLogs.setText("[Aguardando eventos de log...]");
} else {
txtLogs.setText(String.join("\n", logs) + "\n");
}
txtLogs.positionCaret(txtLogs.getText().length());
txtLogs.setScrollTop(Double.MAX_VALUE);
}
private VBox createHeaderLabel(String title, String subtitle) {
@@ -13,8 +13,11 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
@@ -26,21 +29,19 @@ import java.util.Objects;
@SuppressWarnings("this-escape")
public final class MainAppView {
private static final Logger logger = LoggerFactory.getLogger(MainAppView.class);
private final Stage stage;
private final VBox root = new VBox(16);
private final Button themeToggleBtn = new Button();
// Callbacks das Ações
private Runnable onSignPdf;
private Runnable onSignXml;
private Runnable onSignFile;
private Runnable onBatchSign;
private Runnable onAttachedSign;
private Runnable onCoSign;
private Runnable onTimestamp;
private Runnable onLtvSign;
private Runnable onValidateSignature;
private Runnable onValidateTimestamp;
private Runnable onManageSeals;
private Runnable onExit;
@@ -68,7 +69,7 @@ public final class MainAppView {
mainContent.setAlignment(Pos.CENTER);
VBox.setVgrow(mainContent, Priority.ALWAYS);
// --- Hub Central de Ações (Grid 3 colunas x 3 linhas) ---
// --- Hub Central de Ações (Grid 3 colunas x 2 linhas) ---
GridPane actionGrid = new GridPane();
actionGrid.setHgap(12);
actionGrid.setVgap(12);
@@ -80,15 +81,10 @@ public final class MainAppView {
actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0);
actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0);
// Linha 1: Formatos Avançados, Temporais e Identidade Visual
actionGrid.add(createActionButton(SvgIcons.coSignIcon(), "Co-Assinar\nDocumento", () -> trigger(onCoSign)), 0, 1);
actionGrid.add(createActionButton(SvgIcons.timestampIcon(), "Carimbo\ndo Tempo", () -> trigger(onTimestamp)), 1, 1);
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 2, 1);
// Linha 2: Auditoria, Validação e Saída
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 0, 2);
actionGrid.add(createActionButton(SvgIcons.validateTimestampIcon(), "Validar\nCarimbo", () -> trigger(onValidateTimestamp)), 1, 2);
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 2);
// Linha 1: Identidade Visual, Auditoria e Saída
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 0, 1);
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 1, 1);
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 1);
// --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) ---
VBox sideColumn = new VBox(10);
@@ -135,6 +131,14 @@ public final class MainAppView {
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
).toExternalForm());
if (Boolean.getBoolean("cybercore.diag.no-effects")) {
logger.warn("Diagnóstico: Efeitos/sombras visuais DESATIVADOS via -Dcybercore.diag.no-effects=true");
var noEffectsUrl = getClass().getResource("/styles/no-effects.css");
if (noEffectsUrl != null) {
scene.getStylesheets().add(noEffectsUrl.toExternalForm());
}
}
// Configuração e inicialização do Tema
ThemeManager.applyTheme(scene);
updateThemeToggleButton();
@@ -143,12 +147,18 @@ public final class MainAppView {
stage.setTitle(CyberCoreUiConstants.APP_NAME);
stage.setScene(scene);
if (Boolean.getBoolean("cybercore.diag.plain-stage")) {
logger.warn("Diagnóstico: Plain Stage ATIVADO via -Dcybercore.diag.plain-stage=true (janela redimensionável, sem restrições min/max)");
stage.setResizable(true);
} else {
stage.setResizable(false);
stage.setMinWidth(780);
stage.setMinHeight(480);
stage.setMaxWidth(820);
stage.setMaxHeight(520);
}
}
private HBox createHeaderBar() {
HBox headerBar = new HBox(16);
@@ -204,14 +214,20 @@ public final class MainAppView {
private void updateThemeToggleButton() {
boolean dark = ThemeManager.isDark();
HBox content = new HBox(6);
HBox content = new HBox(8);
content.setAlignment(Pos.CENTER);
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.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;");
content.getChildren().addAll(icon, label);
content.getChildren().addAll(iconBox, label);
themeToggleBtn.setGraphic(content);
}
@@ -223,16 +239,22 @@ public final class MainAppView {
btn.setPrefHeight(68);
btn.setMinHeight(68);
HBox content = new HBox(10);
HBox content = new HBox(12);
content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text");
lbl.setWrapText(true);
lbl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(lbl, Priority.ALWAYS);
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 {
content.getChildren().add(lbl);
}
@@ -251,16 +273,22 @@ public final class MainAppView {
btn.setMinHeight(42);
btn.setMaxWidth(Double.MAX_VALUE);
HBox content = new HBox(10);
HBox content = new HBox(12);
content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(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);
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 {
content.getChildren().add(lbl);
}
@@ -300,11 +328,8 @@ public final class MainAppView {
public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; }
public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; }
public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; }
public void setOnCoSign(Runnable onCoSign) { this.onCoSign = onCoSign; }
public void setOnTimestamp(Runnable onTimestamp) { this.onTimestamp = onTimestamp; }
public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; }
public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; }
public void setOnValidateTimestamp(Runnable onValidateTimestamp) { this.onValidateTimestamp = onValidateTimestamp; }
public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; }
public void setOnExit(Runnable onExit) { this.onExit = onExit; }
@@ -0,0 +1,590 @@
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.ManagedCertificate;
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.provider.CertificateManager;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.security.SecurePinStorage;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.File;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Diálogo dedicado e compacto para Seleção de Certificado Digital ICP-Brasil, inserção de PIN/senha,
* definição do arquivo de destino e conclusão da Assinatura Digital PAdES.
*/
@SuppressWarnings("this-escape")
public final class PdfCertificateSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
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,
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
) {
this(visibleSeal, sealTheme, cpfDisplayMode, includeDateTime, customExtraInfo, positionOption,
pageOption, isCustomPosition, customX, customY, customPageNumber, reason, location, 1.0);
}
}
private final Stage stage;
private final CertificateManager certManager;
private final PdfSigningService pdfSigningService;
private final File sourcePdfFile;
private final SealConfig sealConfig;
private final String sealSummaryText;
private ComboBox<ManagedCertificate> certCombo;
private Label lblPinTitle;
private PasswordField pinField;
private Button btnClearRememberedPin;
private Label lblPinNotice;
private CheckBox chkRememberPin;
private ProgressIndicator progressIndicator;
private Label lblStatus;
private Button btnCancel;
private Button btnSign;
public PdfCertificateSignDialog(Stage owner,
CertificateManager certManager,
PdfSigningService pdfSigningService,
File sourcePdfFile,
SealConfig sealConfig,
String sealSummaryText) {
this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
this.pdfSigningService = Objects.requireNonNull(pdfSigningService, "PdfSigningService não pode ser nulo.");
this.sourcePdfFile = Objects.requireNonNull(sourcePdfFile, "sourcePdfFile não pode ser nulo.");
this.sealConfig = Objects.requireNonNull(sealConfig, "sealConfig não pode ser nulo.");
this.sealSummaryText = sealSummaryText != null ? sealSummaryText : "Selo Visual PAdES";
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setResizable(true);
this.stage.setMinWidth(700);
this.stage.setMinHeight(490);
this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
initLayout();
}
private HBox windowsStoreBox;
private HBox pinRow;
private void initLayout() {
VBox root = new VBox(14);
root.setPadding(new Insets(18, 20, 18, 20));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2);
Label lblTitle = new Label("SELEÇÃO DE CERTIFICADO DIGITAL");
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Escolha o certificado ICP-Brasil e informe o PIN para concluir a assinatura digital.");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.certificatesIcon(), titleBox);
// 1. Resumo do Documento e do Selo
VBox summaryBox = new VBox(8);
summaryBox.getStyleClass().addAll("dialog-box", "document-summary-box");
summaryBox.setPadding(new Insets(12, 14, 12, 14));
Label lblSummaryHeader = new Label("DOCUMENTO CONFIGURADO");
lblSummaryHeader.getStyleClass().add("dialog-field-title");
GridPane summaryGrid = new GridPane();
summaryGrid.setHgap(12);
summaryGrid.setVgap(6);
Label lblDocTitle = new Label("Arquivo:");
lblDocTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblDocValue = new Label(sourcePdfFile.getName() + " (" + (sourcePdfFile.length() / 1024) + " KB)");
lblDocValue.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-primary; -fx-font-size: 11.5px;");
lblDocValue.setWrapText(true);
Label lblSealTitle = new Label("Selo Visual:");
lblSealTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblSealValue = new Label(sealSummaryText);
lblSealValue.setStyle("-fx-text-fill: #0284c7; -fx-font-weight: bold; -fx-font-size: 11.5px;");
lblSealValue.setWrapText(true);
summaryGrid.add(lblDocTitle, 0, 0);
summaryGrid.add(lblDocValue, 1, 0);
summaryGrid.add(lblSealTitle, 0, 1);
summaryGrid.add(lblSealValue, 1, 1);
summaryBox.getChildren().addAll(lblSummaryHeader, summaryGrid);
// 2. Card de Certificado ICP-Brasil
VBox certSection = new VBox(10);
certSection.getStyleClass().add("dialog-box");
certSection.setPadding(new Insets(14));
VBox.setVgrow(certSection, Priority.ALWAYS);
HBox certHeader = new HBox(10);
certHeader.setAlignment(Pos.CENTER_LEFT);
Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL");
lblCertTitle.getStyleClass().add("dialog-field-title");
HBox.setHgrow(lblCertTitle, Priority.ALWAYS);
Button btnRefresh = new Button("↻ Atualizar Lista");
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-compact");
btnRefresh.setOnAction(e -> {
populateCertificates();
updatePinFieldVisibility();
});
certHeader.getChildren().addAll(lblCertTitle, btnRefresh);
certCombo = new ComboBox<>();
certCombo.setMaxWidth(Double.MAX_VALUE);
populateCertificates();
certCombo.setOnAction(e -> updatePinFieldVisibility());
certCombo.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} 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() + "]" : "");
setText(holderName + doc + " (" + item.type() + ")");
}
}
});
certCombo.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} 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() + "]" : "");
setText(holderName + doc + " (" + item.type() + ")");
}
}
});
// Banner informativo para certificados da Loja do Windows (sem PIN)
windowsStoreBox = new HBox(8);
windowsStoreBox.setAlignment(Pos.CENTER_LEFT);
windowsStoreBox.setPadding(new Insets(10, 12, 10, 12));
windowsStoreBox.setStyle("-fx-background-color: #ecfdf5; -fx-border-color: #a7f3d0; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblWinNotice = new Label("✓ Certificado instalado no computador — Gerenciado nativamente pelo Windows (PIN não necessário)");
lblWinNotice.setStyle("-fx-text-fill: #065f46; -fx-font-weight: bold; -fx-font-size: 11.5px;");
lblWinNotice.setWrapText(true);
HBox.setHgrow(lblWinNotice, Priority.ALWAYS);
windowsStoreBox.getChildren().add(lblWinNotice);
// Linha de PIN / Senha para Tokens A3 e Arquivos A1
pinRow = new HBox(10);
pinRow.setAlignment(Pos.CENTER_LEFT);
lblPinTitle = new Label("PIN / Senha:");
lblPinTitle.getStyleClass().add("dialog-field-title");
pinField = new PasswordField();
pinField.setPromptText("PIN ou senha do certificado...");
pinField.setPrefWidth(220);
btnClearRememberedPin = new Button("Alterar PIN");
btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact");
btnClearRememberedPin.setStyle("-fx-font-size: 11px;");
btnClearRememberedPin.setVisible(false);
btnClearRememberedPin.setOnAction(e -> {
ManagedCertificate sel = certCombo.getValue();
if (sel != null) {
SecurePinStorage.getInstance().removePin(sel.id());
updatePinFieldVisibility();
}
});
lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11.5px;");
lblPinNotice.setWrapText(true);
HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;");
chkRememberPin.setSelected(true);
VBox tipBox = new VBox(2);
tipBox.setPadding(new Insets(8, 10, 8, 10));
tipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblTip = new Label("🔒 Segurança: O PIN é criptografado localmente com AES-256-GCM vinculado à máquina e nunca é transmitido na rede.");
lblTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
lblTip.setWrapText(true);
lblTip.setMaxWidth(Double.MAX_VALUE);
tipBox.getChildren().add(lblTip);
certSection.getChildren().addAll(certHeader, certCombo, windowsStoreBox, pinRow, chkRememberPin, tipBox);
// 3. Rodapé
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
footer.setPadding(new Insets(4, 0, 0, 0));
progressIndicator = new ProgressIndicator();
progressIndicator.setPrefSize(22, 22);
progressIndicator.setVisible(false);
lblStatus = new Label();
lblStatus.setStyle("-fx-font-size: 12px;");
HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCancel.setMinWidth(110);
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento PDF");
btnSign.getStyleClass().addAll("btn-primary", "btn-dialog-wide");
btnSign.setMinWidth(220);
btnSign.setPrefWidth(Region.USE_COMPUTED_SIZE);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> handleSign());
footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign);
root.getChildren().addAll(header, summaryBox, certSection, footer);
Scene scene = new Scene(root, 720, 500);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
updatePinFieldVisibility();
}
private void populateCertificates() {
try {
List<ManagedCertificate> certs = certManager.getAvailableCertificates();
certCombo.setItems(FXCollections.observableArrayList(certs));
if (!certs.isEmpty()) {
certCombo.setValue(certs.get(0));
}
} catch (Exception e) {
logger.error("Erro ao listar certificados", e);
}
}
private void updatePinFieldVisibility() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
pinField.setDisable(true);
lblPinNotice.setText("");
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false);
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
if (isWindowsStore) {
// Certificado instalado no repositório do Windows (não exige PIN no aplicativo)
windowsStoreBox.setVisible(true);
windowsStoreBox.setManaged(true);
pinRow.setVisible(false);
pinRow.setManaged(false);
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false);
return;
}
// Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
char[] storedPin = SecurePinStorage.getInstance().getPin(cert.id());
if (storedPin != null && storedPin.length > 0) {
pinField.setText(new String(storedPin));
pinField.setDisable(true);
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(true);
lblPinNotice.setText("✓ PIN lembrado com segurança neste computador");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;");
} else {
pinField.clear();
pinField.setDisable(false);
chkRememberPin.setVisible(true);
chkRememberPin.setManaged(true);
btnClearRememberedPin.setVisible(false);
boolean isA3 = cert.type() == CertificateType.A3;
lblPinNotice.setText(isA3 ? "Requer PIN do token" : "Requer senha do certificado");
lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
}
}
private void handleSign() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital.");
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
char[] storedPin = !isWindowsStore ? SecurePinStorage.getInstance().getPin(cert.id()) : null;
if (!isWindowsStore && (storedPin == null || storedPin.length == 0) && (pinField.getText() == null || pinField.getText().trim().isEmpty())) {
showAlert(Alert.AlertType.WARNING, "PIN / Senha Obrigatório",
cert.type() == CertificateType.A3
? "Por favor, informe o PIN do seu dispositivo A3 para autorizar a assinatura."
: "Por favor, informe a senha do certificado A1 para autorizar a assinatura.");
pinField.requestFocus();
return;
}
FileChooser chooser = new FileChooser();
chooser.setTitle("Salvar PDF Assinado Como...");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
String originalName = sourcePdfFile.getName();
String baseName = originalName.toLowerCase().endsWith(".pdf")
? originalName.substring(0, originalName.length() - 4)
: originalName;
chooser.setInitialFileName(baseName + "_assinado.pdf");
if (sourcePdfFile.getParentFile() != null && sourcePdfFile.getParentFile().exists()) {
chooser.setInitialDirectory(sourcePdfFile.getParentFile());
}
File targetFile = chooser.showSaveDialog(stage);
if (targetFile == null) {
return;
}
char[] pinToUse = null;
if (!isWindowsStore) {
if (storedPin != null && storedPin.length > 0) {
pinToUse = storedPin;
} else if (!pinField.getText().isEmpty()) {
pinToUse = pinField.getText().toCharArray();
}
}
final char[] enteredPin = (!isWindowsStore && pinField.getText() != null && !pinField.getText().isEmpty())
? pinField.getText().toCharArray()
: null;
PdfSignatureOptions.Builder b = PdfSignatureOptions.builder(cert.id())
.pinOrPassword(pinToUse)
.visibleSeal(sealConfig.visibleSeal())
.sealTheme(sealConfig.sealTheme())
.cpfDisplayMode(sealConfig.cpfDisplayMode())
.includeDateTime(sealConfig.includeDateTime())
.customExtraInfo(sealConfig.customExtraInfo())
.reason(sealConfig.reason())
.location(sealConfig.location())
.customSealScale(sealConfig.customSealScale());
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
} else if (sealConfig.positionOption() != null) {
b.position(sealConfig.positionOption().toPosition());
if (sealConfig.pageOption() != null) {
b.pageChoice(sealConfig.pageOption().toPageChoice());
}
}
PdfSignatureOptions options = b.build();
setFormDisabled(true);
progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento PDF com envelope PAdES...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
Task<File> signTask = new Task<>() {
@Override
protected File call() throws Exception {
pdfSigningService.signPdfFile(sourcePdfFile, targetFile, options);
return targetFile;
}
};
signTask.setOnSucceeded(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
lblStatus.setText("✓ Documento assinado com sucesso!");
lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
// Salva o PIN de forma segura se solicitado pelo usuário
if (!isWindowsStore && chkRememberPin.isSelected() && enteredPin != null && enteredPin.length > 0) {
SecurePinStorage.getInstance().savePin(cert.id(), enteredPin);
}
showSuccessDialog(targetFile);
stage.close();
});
signTask.setOnFailed(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
Throwable ex = signTask.getException();
logger.error("Falha ao assinar PDF", ex);
// Se o PIN estava incorreto, remove do armazenamento seguro
String msg = ex != null ? ex.getMessage() : "";
if (msg.contains("PIN_INCORRECT") || msg.contains("senha") || msg.contains("PIN")) {
SecurePinStorage.getInstance().removePin(cert.id());
updatePinFieldVisibility();
}
lblStatus.setText("✕ Falha na assinatura");
lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;");
showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do PDF",
"Não foi possível concluir a assinatura digital:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido"));
});
Thread signThread = new Thread(signTask, "cybercore-pdf-cert-signer");
signThread.setDaemon(true);
signThread.start();
}
private void showSuccessDialog(File targetFile) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Assinatura Concluída");
alert.setHeaderText("PDF Assinado com Sucesso!");
alert.setContentText("O documento foi digitalmente assinado no padrão PAdES ICP-Brasil e salvo no destino solicitado.\n\n"
+ "Arquivo gerado:\n" + targetFile.getAbsolutePath());
ButtonType btnOpen = new ButtonType("Abrir PDF");
ButtonType btnFolder = new ButtonType("Abrir Pasta");
ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent()) {
if (result.get() == btnOpen) {
openFile(targetFile);
} else if (result.get() == btnFolder) {
openFolder(targetFile);
}
}
}
private void openFile(File file) {
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
Desktop.getDesktop().open(file);
}
} catch (Exception ex) {
logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage());
}
}
private void openFolder(File file) {
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
File parent = file.getParentFile();
if (parent != null) {
Desktop.getDesktop().open(parent);
}
}
} catch (Exception ex) {
logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage());
}
}
private void setFormDisabled(boolean disabled) {
btnSign.setDisable(disabled);
btnCancel.setDisable(disabled);
certCombo.setDisable(disabled);
pinField.setDisable(disabled);
}
private void showAlert(Alert.AlertType type, String title, String message) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
alert.showAndWait();
}
public void show() {
stage.centerOnScreen();
stage.show();
stage.toFront();
}
}
File diff suppressed because it is too large Load Diff
@@ -222,8 +222,9 @@ public final class SealManagementDialog {
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
@@ -124,7 +124,7 @@ public final class SettingsView extends VBox {
HBox pinActionsBox = new HBox(12);
pinActionsBox.setAlignment(Pos.CENTER_LEFT);
Button btnClearPins = new Button("Limpar PINs Salvos na Sessão");
Button btnClearPins = new Button("Limpar PINs Salvos (Sessão e Computador)");
btnClearPins.getStyleClass().add("btn-secondary");
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
@@ -133,8 +133,9 @@ public final class SettingsView extends VBox {
btnClearPins.setOnAction(e -> {
PinSessionManager.getInstance().clearAll();
br.com.cybercore.signer.core.security.SecurePinStorage.getInstance().clearAll();
SmartCardSessionManager.resetCardSessions();
pinStatusLabel.setText("✓ Todos os PINs e sessões de tokens foram zerados.");
pinStatusLabel.setText("✓ Todos os PINs salvos em memória e neste computador foram zerados.");
});
pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel);
@@ -193,6 +194,8 @@ public final class SettingsView extends VBox {
private void refreshSealsList() {
sealsContainer.getChildren().clear();
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
AgentConfigurationService configService = AgentConfigurationService.getInstance();
String currentDefaultId = configService.getConfig().defaultSealThemeId();
for (PdfSealTheme theme : themes) {
HBox item = new HBox(10);
@@ -200,6 +203,8 @@ public final class SettingsView extends VBox {
item.getStyleClass().add("diagnostic-item");
item.setPadding(new Insets(6, 12, 6, 12));
boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultId);
VBox info = new VBox(2);
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
lblName.getStyleClass().add("cert-title");
@@ -211,10 +216,34 @@ public final class SettingsView extends VBox {
info.getChildren().addAll(lblName, lblDesc);
HBox.setHgrow(info, Priority.ALWAYS);
Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
Label badgeType = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
badgeType.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
item.getChildren().addAll(info, badgeType);
item.getChildren().addAll(info, badge);
if (isDefault) {
Label badgeDefault = new Label("★ PADRÃO");
badgeDefault.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-padding: 3px 8px; -fx-background-radius: 4px; -fx-font-weight: bold; -fx-font-size: 11px;");
item.getChildren().add(badgeDefault);
} else {
Button btnSetDefault = new Button("★ Definir Padrão");
btnSetDefault.getStyleClass().addAll("btn-secondary", "btn-compact");
btnSetDefault.setStyle("-fx-padding: 3px 8px; -fx-font-size: 11px;");
btnSetDefault.setOnAction(e -> {
AgentConfiguration current = configService.getConfig();
configService.updateConfig(new AgentConfiguration(
current.requireConfirmation(),
current.alwaysPromptPin(),
current.autoStart(),
theme.id(),
current.defaultCpfDisplayMode(),
current.defaultIncludeDateTime(),
current.serverPort()
));
refreshSealsList();
notifySave("Selo '" + theme.name() + "' definido como o padrão para assinaturas.");
});
item.getChildren().add(btnSetDefault);
}
if (theme.isCustom()) {
Button btnDelete = new Button("Excluir");
@@ -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.security.PinSessionManager;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -56,17 +57,33 @@ public class SignaturePromptDialog {
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
String documentName, String documentType, boolean isAuthentication) {
VBox root = new VBox(18);
VBox root = new VBox(16);
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");
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 a assinatura digital de um documento.");
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
VBox detailsBox = new VBox(12);
@@ -89,9 +106,24 @@ public class SignaturePromptDialog {
}
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
}
addField(grid, row++, "Certificado:", cert.commonName() != null ? cert.commonName() : cert.alias());
addField(grid, row++, "Tipo / Dispositivo:", cert.type().getCode() + " • " +
(cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.type().getLabel()));
String rawCertName = cert.commonName() != null ? cert.commonName() : cert.alias();
String holderName = rawCertName.contains(":") ? rawCertName.split(":")[0].trim() : rawCertName.trim();
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());
detailsBox.getChildren().add(grid);
@@ -117,7 +149,7 @@ public class SignaturePromptDialog {
boolean isA3 = cert.type() == CertificateType.A3;
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(cert.id());
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(context != null ? context.applicationId() : null, cert.id());
if (isA3) {
pinContainer.getStyleClass().add("dialog-box");
@@ -134,7 +166,8 @@ public class SignaturePromptDialog {
"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."
);
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);
winBox.getChildren().addAll(winTitle, winDesc);
@@ -166,6 +199,7 @@ public class SignaturePromptDialog {
if (nowVisible) {
cbSavePin.setSelected(true);
}
stage.sizeToScene();
});
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
@@ -173,25 +207,26 @@ public class SignaturePromptDialog {
} else {
VBox inputWrapper = new VBox(8);
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);
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
pinContainer.getChildren().add(inputWrapper);
}
}
detailsBox.getChildren().add(pinContainer);
}
// Label de Alerta de Validação
Label validationLabel = new Label();
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
validationLabel.setVisible(false);
validationLabel.setManaged(false);
// Botões de Ação
HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
actionsRow.setPadding(new Insets(4, 0, 0, 0));
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-cancel");
@@ -211,6 +246,8 @@ public class SignaturePromptDialog {
if (!hasStoredPin && !pinProvided) {
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
validationLabel.setVisible(true);
validationLabel.setManaged(true);
stage.sizeToScene();
pinField.requestFocus();
return;
}
@@ -218,9 +255,9 @@ public class SignaturePromptDialog {
if (pinProvided) {
char[] pinChars = typedPin.toCharArray();
if (cbSavePin.isSelected()) {
PinSessionManager.getInstance().savePin(cert.id(), pinChars);
PinSessionManager.getInstance().savePin(context != null ? context.applicationId() : null, cert.id(), pinChars);
} else {
PinSessionManager.getInstance().clearPin(cert.id());
PinSessionManager.getInstance().clearPin(context != null ? context.applicationId() : null, cert.id());
}
}
}
@@ -231,12 +268,18 @@ public class SignaturePromptDialog {
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);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
stage.sizeToScene();
stage.centerOnScreen();
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
}
@@ -252,6 +295,8 @@ public class SignaturePromptDialog {
}
public CompletableFuture<Boolean> showAndAwait() {
stage.sizeToScene();
stage.centerOnScreen();
stage.show();
stage.toFront();
return approvalFuture;
@@ -40,7 +40,6 @@ public final class SignatureValidationDialog {
private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class);
private final Stage stage;
private final boolean isTimestamp;
private final SignatureValidationService validationService = new SignatureValidationService();
private RadioButton rbAttached;
@@ -58,15 +57,13 @@ public final class SignatureValidationDialog {
private Button btnSaveExtracted;
private byte[] lastExtractedContent;
public SignatureValidationDialog(Stage owner, boolean isTimestamp) {
this.isTimestamp = isTimestamp;
public SignatureValidationDialog(Stage owner) {
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle(isTimestamp ? "CyberCore Signer — Validação de Carimbo do Tempo" : "CyberCore Signer — Validação de Assinatura Digital");
this.stage.setTitle("CyberCore Signer — Validação de Assinatura Digital");
this.stage.setResizable(true);
this.stage.setMinWidth(680);
this.stage.setMinHeight(520);
@@ -82,11 +79,10 @@ public final class SignatureValidationDialog {
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
isTimestamp ? SvgIcons.validateTimestampIcon() : SvgIcons.validateIcon(),
SvgIcons.validateIcon(),
createHeaderLabel(
isTimestamp ? "VALIDAÇÃO DE CARIMBO DO TEMPO" : "VALIDAÇÃO DE ASSINATURA DIGITAL",
isTimestamp ? "Verifique a autenticidade e conformidade RFC 3161 TSP."
: "Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
"VALIDAÇÃO DE ASSINATURA DIGITAL",
"Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
)
);
@@ -110,11 +106,6 @@ public final class SignatureValidationDialog {
radioRow.getChildren().addAll(rbAttached, rbDetached);
typeBox.getChildren().addAll(lblTypeTitle, radioRow);
if (isTimestamp) {
typeBox.setVisible(false);
typeBox.setManaged(false);
}
// 1. Arquivo de Assinatura / Arquivo Assinado
VBox sigBox = new VBox(8);
sigBox.getStyleClass().add("dialog-box");
@@ -197,7 +188,7 @@ public final class SignatureValidationDialog {
}
private void updateModeView() {
boolean isAttached = rbAttached.isSelected() && !isTimestamp;
boolean isAttached = rbAttached.isSelected();
docBox.setVisible(!isAttached);
docBox.setManaged(!isAttached);
@@ -205,7 +196,7 @@ public final class SignatureValidationDialog {
lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos...");
} else {
lblSigPath.setText(isTimestamp ? "Arquivo de Carimbo (.tsr, .tsp, .p7s):" : "Arquivo de Assinatura (.p7s, .sig):");
lblSigPath.setText("Arquivo de Assinatura (.p7s, .sig):");
txtSigPath.setPromptText("Selecione o arquivo de assinatura digital...");
}
}
@@ -227,7 +218,7 @@ public final class SignatureValidationDialog {
if (rbAttached.isSelected()) {
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*"));
} else {
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig, *.tsr)", "*.p7s", "*.sig", "*.tsr", "*.*"));
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig)", "*.p7s", "*.sig", "*.*"));
}
File f = ch.showOpenDialog(stage);
@@ -249,7 +240,7 @@ public final class SignatureValidationDialog {
return;
}
boolean isDetachedMode = rbDetached.isSelected() || isTimestamp;
boolean isDetachedMode = rbDetached.isSelected();
if (isDetachedMode && (docFile == null || !docFile.exists())) {
showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado",
"Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura.");
@@ -77,8 +77,9 @@ public final class TutorialDialog {
HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
@@ -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.security.PinSessionManager;
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.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
@@ -25,11 +26,14 @@ import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
@@ -66,7 +70,9 @@ public final class XmlSignDialog {
private PasswordField pinField;
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 rbSpecificElement;
private TextField txtElementTag;
@@ -92,10 +98,10 @@ public final class XmlSignDialog {
if (owner != null) {
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.setMinWidth(720);
this.stage.setMinHeight(580);
this.stage.setMinHeight(600);
initLayout();
}
@@ -109,9 +115,9 @@ public final class XmlSignDialog {
HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT);
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");
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");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
@@ -196,7 +202,21 @@ public final class XmlSignDialog {
// 3. Seção: Modo de Assinatura XMLDSig
VBox xmlModeSection = new VBox(12);
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");
ToggleGroup modeGroup = new ToggleGroup();
@@ -214,6 +234,16 @@ public final class XmlSignDialog {
elementGrid.setHgap(12);
elementGrid.setVgap(8);
ColumnConstraints colTagLabel = new ColumnConstraints();
colTagLabel.setMinWidth(140);
colTagLabel.setPrefWidth(145);
colTagLabel.setHgrow(Priority.NEVER);
ColumnConstraints colTagField = new ColumnConstraints();
colTagField.setHgrow(Priority.ALWAYS);
elementGrid.getColumnConstraints().addAll(colTagLabel, colTagField);
Label lblTag = new Label("Tag do Elemento:");
lblTag.getStyleClass().add("dialog-field-title");
txtElementTag = new TextField("infNFe");
@@ -235,7 +265,16 @@ public final class XmlSignDialog {
chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação");
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);
scrollPane.setContent(formContent);
@@ -254,11 +293,12 @@ public final class XmlSignDialog {
HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento XML");
btnSign.getStyleClass().add("btn-primary");
btnSign.getStyleClass().addAll("btn-primary", "btn-dialog");
btnSign.setMinWidth(Region.USE_PREF_SIZE);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> executeXmlSignature());
@@ -376,6 +416,10 @@ public final class XmlSignDialog {
pin = pinField.getText().toCharArray();
}
XmlSignatureProfile profile = rbXadesBes.isSelected()
? XmlSignatureProfile.XADES_BES
: XmlSignatureProfile.XMLDSIG;
XmlSignatureOptions options;
if (rbSpecificElement.isSelected()) {
String tag = txtElementTag.getText().trim();
@@ -383,14 +427,14 @@ public final class XmlSignDialog {
if (tag.isBlank()) {
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 {
options = XmlSignatureOptions.wholeDocument(cert.id(), pin);
options = XmlSignatureOptions.wholeDocument(cert.id(), pin, profile);
}
setFormDisabled(true);
progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento XML (XMLDSig)...");
lblStatus.setText("Assinando documento XML (" + profile.getDisplayName() + ")...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
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
│ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── 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)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
└── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
├── app-icon.png # Fallback em PNG (256x256 px)
├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
├── 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
1. **Ícones:**
- Estão no formato `.svg` padrão.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou 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.
- Todos estão no formato `.svg` padrão da W3C.
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
- 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:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
- 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.
- 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 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:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
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!
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
3. **Ordem de Resolução e Carregamento:**
Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
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/`)**: Para customizações no diretório pessoal do usuário.
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 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">
<circle cx="24" cy="24" r="20" fill="#0288d1"/>
<circle cx="24" cy="14" r="3" fill="#ffffff"/>
<line x1="24" y1="21" x2="24" y2="34" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
<!-- 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: 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">
<!-- Lua crescente ciano/índigo -->
<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"/>
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/>
<circle cx="42" cy="18" r="1" fill="#818cf8"/>
<!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
<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="34" cy="14" r="1.8" fill="#818cf8"/>
<circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 344 B

@@ -4,10 +4,10 @@
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta vermelha com sigla PDF -->
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8.5" font-weight="bold" fill="#ffffff" text-anchor="middle">PDF</text>
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta vermelha com sigla PDF perfeitamente centralizada -->
<rect x="11" y="31" width="28" height="11" rx="3" fill="#d32f2f"/>
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8.5px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">PDF</text>
</svg>

Before

Width:  |  Height:  |  Size: 996 B

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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">
<!-- Engrenagem simétrica de 8 dentes 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"/>
<!-- 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"/>
<!-- Engrenagem corporativa simétrica centrada em (24, 24) -->
<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"/>
</svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 784 B

@@ -4,10 +4,10 @@
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta azul com sigla XML -->
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8" font-weight="bold" fill="#ffffff" text-anchor="middle">&lt;XML&gt;</text>
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta azul com sigla XML perfeitamente centralizada -->
<rect x="11" y="31" width="28" height="11" rx="3" fill="#0288d1"/>
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">&lt;XML&gt;</text>
</svg>

Before

Width:  |  Height:  |  Size: 998 B

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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

Some files were not shown because too many files have changed in this diff Show More