feat: initial commit - cybercore signer desktop agent
@@ -0,0 +1,250 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
PROMPT MESTRE — CYBERCORE SIGNER
|
||||
|
||||
Você é responsável pelo desenvolvimento do CyberCore Signer, um agente desktop Java para Windows que permite aplicações web autorizadas solicitar assinaturas digitais usando certificados instalados/localmente disponíveis na máquina do usuário.
|
||||
|
||||
1. REGRA DE DESENVOLVIMENTO
|
||||
|
||||
Siga sempre: ANALISAR → PLANEJAR → AUTORIZAR → IMPLEMENTAR → VALIDAR.
|
||||
|
||||
Antes de alterar qualquer código, analise o repositório real, sua estrutura, dependências, configurações, padrões, documentação e testes. Não invente arquivos, classes ou arquitetura existentes.
|
||||
|
||||
Na fase de análise, não altere o projeto. Depois apresente o plano e aguarde autorização explícita para implementar.
|
||||
|
||||
Se durante a implementação surgir mudança relevante de escopo, pare e peça nova autorização.
|
||||
|
||||
Ao finalizar, informe arquivos alterados/criados, dependências, decisões arquiteturais, testes executados, limitações e pendências.
|
||||
|
||||
2. TECNOLOGIA
|
||||
|
||||
Use Java 21+ LTS, Maven ou Gradle conforme o projeto, JavaFX para a interface e JCA/JCE para criptografia.
|
||||
|
||||
Use X.509, KeyStore/PKCS12 e PKCS#11 conforme necessário. Use SunPKCS11 quando apropriado e Bouncy Castle somente quando houver necessidade real.
|
||||
|
||||
O aplicativo deve ser empacotado com runtime próprio, preferencialmente via jpackage, sem exigir Java instalado pelo usuário.
|
||||
|
||||
Windows 10/11 é o alvo inicial; mantenha a arquitetura preparada para macOS/Linux.
|
||||
|
||||
3. ARQUITETURA
|
||||
|
||||
Separe responsabilidades em módulos/componentes coerentes, sem criar módulos artificiais. A arquitetura deve permitir evolução e testes independentes.
|
||||
|
||||
Crie abstrações para descoberta e uso de certificados, por exemplo CertificateProvider, com implementações para A1, Windows Certificate Store e PKCS#11 quando aplicável.
|
||||
|
||||
CertificateManager deve centralizar descoberta, validade, subject, issuer, algoritmo, disponibilidade da chave privada e seleção do provider.
|
||||
|
||||
SigningService deve executar a assinatura por JCA/JCE e providers; nunca implemente criptografia própria.
|
||||
|
||||
Mantenha o núcleo independente de fabricantes.
|
||||
|
||||
4. A1
|
||||
|
||||
Suporte certificados .PFX/.P12.
|
||||
|
||||
A chave privada permanece exclusivamente na máquina. A senha nunca deve ser enviada ao servidor nem aparecer em logs.
|
||||
|
||||
Trate certificado válido, expirado, inválido, senha incorreta, ausência de chave privada e algoritmos não suportados.
|
||||
|
||||
Se houver persistência de credenciais, use armazenamento seguro do sistema operacional.
|
||||
|
||||
5. A3 / PKCS#11
|
||||
|
||||
Suporte tokens USB, smart cards, leitores e middleware compatíveis com PKCS#11, além de cenários Windows CSP/CNG/KSP quando necessários.
|
||||
|
||||
Detecte slots, tokens, certificados e chaves disponíveis.
|
||||
|
||||
Trate token ausente/removido, middleware/provider indisponível, PIN requerido/incorreto, timeout e falhas do dispositivo.
|
||||
|
||||
Nunca armazene ou envie PIN. Nunca implemente sua própria camada PKCS#11: utilize APIs/providers existentes.
|
||||
|
||||
Não assuma que todos os A3 funcionam da mesma forma; use adapters/providers sem acoplar o núcleo a um fabricante.
|
||||
|
||||
6. ASSINATURA
|
||||
|
||||
Priorize o modelo em que a aplicação web calcula o digest e o CyberCore assina localmente apenas esse digest.
|
||||
|
||||
Exemplo: documento → SHA-256 → digest → CyberCore → certificado A1/A3 → assinatura.
|
||||
|
||||
A chave privada nunca sai da máquina.
|
||||
|
||||
Valide algoritmo, tamanho/formato do digest, certificado e capacidade do provider antes da operação.
|
||||
|
||||
7. PROTOCOLO CCSP
|
||||
|
||||
Defina o CyberCore Signing Protocol (CCSP), versão inicial CCSP/1.
|
||||
|
||||
Toda requisição deve possuir request_id UUID, application_id, operação, algoritmo, digest, indicação opcional do certificado e expiração.
|
||||
|
||||
Exemplo:
|
||||
|
||||
{
|
||||
"version":"1","request_id":"uuid","application_id":"uuid",
|
||||
"operation":"sign","algorithm":"SHA256withRSA",
|
||||
"digest":"base64","certificate_hint":"uuid","expires_at":"..."
|
||||
}
|
||||
|
||||
Resposta:
|
||||
|
||||
{
|
||||
"version":"1","request_id":"uuid","status":"signed",
|
||||
"signature":"base64","certificate":"base64","algorithm":"SHA256withRSA"
|
||||
}
|
||||
|
||||
Especifique em documentação autenticação, autorização, validação, timeouts, nonce, replay protection, erros e compatibilidade de versões.
|
||||
|
||||
8. COMUNICAÇÃO LOCAL
|
||||
|
||||
A comunicação deve ocorrer somente localmente por padrão. Avalie HTTPS localhost, WebSocket ou outro mecanismo adequado.
|
||||
|
||||
Nunca exponha uma API de assinatura local sem autenticação/autorização.
|
||||
|
||||
Não permita exposição involuntária à rede LAN.
|
||||
|
||||
Use registro/autorização de aplicações, origem permitida, desafio/nonce, timestamp, expiração e proteção contra replay.
|
||||
|
||||
Não confie exclusivamente em Origin ou User-Agent.
|
||||
|
||||
9. SEGURANÇA
|
||||
|
||||
A assinatura deve exigir confirmação explícita do usuário por padrão.
|
||||
|
||||
Mostre aplicação solicitante, certificado, tipo A1/A3, algoritmo e informações relevantes antes de assinar.
|
||||
|
||||
Nunca registre PIN, senha, chave privada ou conteúdo desnecessário do documento.
|
||||
|
||||
Minimize dados trafegados e armazenados.
|
||||
|
||||
Use UUIDs, correlação por request_id e validação rigorosa de entrada.
|
||||
|
||||
10. INTERFACE
|
||||
|
||||
Use JavaFX com aparência profissional e consistente.
|
||||
|
||||
Inclua, conforme o escopo: Dashboard, Certificados, Solicitação de Assinatura, Configurações, Diagnóstico e Sobre.
|
||||
|
||||
Suporte System Tray.
|
||||
|
||||
A interface deve informar claramente estado do agente, certificados disponíveis, token conectado e erros acionáveis.
|
||||
|
||||
11. DIAGNÓSTICO
|
||||
|
||||
Disponibilize diagnóstico de Java/runtime, engine criptográfica, Windows Certificate Store, A1, A3, PKCS#11, comunicação local e SDK.
|
||||
|
||||
Permita diagnóstico completo e teste de assinatura.
|
||||
|
||||
Diferencie claramente falha de configuração, middleware, token, certificado, comunicação e aplicação solicitante.
|
||||
|
||||
12. ERROS
|
||||
|
||||
Utilize erros estruturados e estáveis, incluindo:
|
||||
|
||||
CERTIFICATE_NOT_FOUND, CERTIFICATE_EXPIRED, CERTIFICATE_INVALID,
|
||||
PRIVATE_KEY_NOT_AVAILABLE, TOKEN_NOT_FOUND, TOKEN_REMOVED,
|
||||
PIN_REQUIRED, PIN_INCORRECT, PKCS11_ERROR, SIGNATURE_FAILED,
|
||||
APPLICATION_NOT_AUTHORIZED, REQUEST_EXPIRED, REPLAY_DETECTED,
|
||||
INVALID_ORIGIN, UNSUPPORTED_ALGORITHM, USER_CANCELLED,
|
||||
PROVIDER_NOT_AVAILABLE e COMMUNICATION_ERROR.
|
||||
|
||||
Mensagens para o usuário devem ser claras, sem expor segredos ou detalhes internos desnecessários.
|
||||
|
||||
13. LOGS E AUDITORIA
|
||||
|
||||
Registre eventos relevantes: inicialização, descoberta de certificados, provider utilizado, aplicação, request_id, operação, sucesso/falha e remoção de token.
|
||||
|
||||
Nunca registre PIN, senha, chave privada ou dados sensíveis desnecessários.
|
||||
|
||||
Use níveis de log adequados e correlação por request_id.
|
||||
|
||||
14. SDK WEB
|
||||
|
||||
Planeje o pacote JavaScript @cybercore/signer-sdk.
|
||||
|
||||
API mínima:
|
||||
|
||||
connect(), isInstalled(), getVersion(), getStatus(),
|
||||
getCertificates(), getCapabilities(), sign(), verify().
|
||||
|
||||
O SDK deve abstrair o CCSP e fornecer erros previsíveis para aplicações web.
|
||||
|
||||
15. MULTIAPLICAÇÃO E CONCORRÊNCIA
|
||||
|
||||
Uma única instalação deve atender múltiplas aplicações web autorizadas.
|
||||
|
||||
Controle concorrência e fila de operações, especialmente em A3, pois tokens/middleware podem não suportar operações simultâneas.
|
||||
|
||||
Nunca misture contexto ou autorização de uma aplicação com outra.
|
||||
|
||||
16. PDF/PAdES
|
||||
|
||||
Não misture a operação criptográfica básica com a montagem/finalização de PDF.
|
||||
|
||||
Para PAdES, avalie bibliotecas maduras como Apache PDFBox, Bouncy Castle ou DSS.
|
||||
|
||||
Nunca implemente manualmente mecanismos criptográficos ou estruturas complexas de assinatura PDF sem necessidade.
|
||||
|
||||
17. TESTES
|
||||
|
||||
Crie testes unitários, integração, segurança, protocolo, providers, comunicação local, SDK e concorrência.
|
||||
|
||||
Cubra A1 válido/expirado/inválido/senha incorreta; A3 sem token, token removido, PIN incorreto, middleware indisponível e cancelamento; assinatura válida; algoritmo incompatível; requisição expirada/repetida/não autorizada; origem inválida; payload malformado e concorrência.
|
||||
|
||||
Testes dependentes de hardware só podem ser declarados executados quando o hardware realmente estiver disponível.
|
||||
|
||||
18. MODO DESENVOLVIMENTO
|
||||
|
||||
Implemente mock quando útil para desenvolvimento e testes, simulando A1/A3, token removido, PIN incorreto, certificado expirado, provider indisponível, timeout e cancelamento.
|
||||
|
||||
O modo deve ser claramente identificado como DESENVOLVIMENTO.
|
||||
|
||||
Nunca apresente assinatura simulada como assinatura real.
|
||||
|
||||
19. INSTALADOR E ATUALIZAÇÃO
|
||||
|
||||
Crie instalador Windows com runtime próprio, instalação/desinstalação, atalhos e opções de inicialização quando aplicável.
|
||||
|
||||
Atualizações devem ser autenticadas e seguras.
|
||||
|
||||
Executáveis e instaladores devem ser preparados para code signing.
|
||||
|
||||
20. PRIVACIDADE
|
||||
|
||||
A chave privada, PIN e senha A1 permanecem na máquina.
|
||||
|
||||
Quando apenas o digest for necessário, o documento não deve ser enviado ao CyberCore.
|
||||
|
||||
Colete, armazene e registre somente o necessário para operação, diagnóstico e auditoria.
|
||||
|
||||
21. DOCUMENTAÇÃO
|
||||
|
||||
Mantenha documentação técnica em docs/.
|
||||
|
||||
Documente especialmente docs/CCSP.md, arquitetura, instalação, certificados A1/A3, PKCS#11, segurança, SDK, erros, diagnóstico, desenvolvimento e limitações.
|
||||
|
||||
Toda mudança relevante de protocolo ou arquitetura deve atualizar a documentação correspondente.
|
||||
|
||||
22. IMPLEMENTAÇÃO
|
||||
|
||||
Ao receber uma tarefa:
|
||||
|
||||
1) ANALISE o projeto real e identifique impacto, dependências e riscos.
|
||||
2) PLANEJE a solução, apresentando arquivos/componentes afetados e estratégia de testes.
|
||||
3) PEÇA AUTORIZAÇÃO explícita antes de modificar o código.
|
||||
4) IMPLEMENTE seguindo os padrões existentes e evitando alterações fora do escopo.
|
||||
5) VALIDE com build, testes, análise de erros e, quando possível, execução real.
|
||||
|
||||
Não marque uma tarefa como concluída apenas porque o código compila.
|
||||
|
||||
23. REGRA FINAL
|
||||
|
||||
Priorize segurança, interoperabilidade, rastreabilidade, manutenção e compatibilidade.
|
||||
|
||||
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.
|
||||
|
||||
Princípio obrigatório:
|
||||
|
||||
ANALISAR → PLANEJAR → AUTORIZAR → IMPLEMENTAR → VALIDAR.
|
||||
@@ -0,0 +1,80 @@
|
||||
# ==============================================================================
|
||||
# CyberCore Signer - .gitignore
|
||||
# ==============================================================================
|
||||
|
||||
# Compilação e Artefatos Maven
|
||||
target/
|
||||
**/target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Binários gerados e Distribuições Locais
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.zip
|
||||
dist/
|
||||
build/
|
||||
bin/
|
||||
|
||||
# Certificados, Chaves e Credenciais Locais (Privacidade e Segurança)
|
||||
*.pfx
|
||||
*.p12
|
||||
*.jks
|
||||
*.keystore
|
||||
*.key
|
||||
*.pem
|
||||
*.cer
|
||||
*.crt
|
||||
*.der
|
||||
!assets/
|
||||
!docs/
|
||||
|
||||
# Ambientes e Variáveis Locais
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDEs e Editores
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
.factorypath
|
||||
bin/
|
||||
|
||||
# Logs e Auditorias de Runtime Locais
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Node / SDK de Testes
|
||||
node_modules/
|
||||
sdk/node_modules/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Sistema Operacional
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.tmp
|
||||
*~
|
||||
|
||||
# Arquivos temporários de Scratch locais
|
||||
scratch/temp/
|
||||
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
@@ -0,0 +1,100 @@
|
||||
# CyberCore Signer
|
||||
|
||||
> **Agente Desktop 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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🏛 Arquitetura do Projeto
|
||||
|
||||
O projeto é estruturado em módulos Maven:
|
||||
|
||||
* **[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.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Requisitos
|
||||
|
||||
* **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).
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Compilação e Empacotamento
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Princípios de Segurança e Privacidade
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentação Técnica
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Pasta de Ícones (SVG) e Imagens (PNG) — 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**.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Estrutura de Arquivos Criados
|
||||
|
||||
```
|
||||
assets/
|
||||
├── icons/ # Ícones vetoriais SVG reais e editáveis (W3C padrão)
|
||||
│ ├── pdf.svg # Ícone Assinar PDF (documento com tag vermelha PDF)
|
||||
│ ├── xml.svg # Ícone Assinar XML (documento com tag azul <XML>)
|
||||
│ ├── file.svg # Ícone Assinar Arquivo (documento com caneta)
|
||||
│ ├── batch.svg # Ícone Assinatura em Lote (pilha de documentos com check)
|
||||
│ ├── attached.svg # Ícone Assinatura Atachada (documento com clipe de papel)
|
||||
│ ├── co-sign.svg # Ícone Co-Assinar (documentos duplos com usuários)
|
||||
│ ├── timestamp.svg # Ícone Carimbo do Tempo (documento com relógio TSA)
|
||||
│ ├── ltv.svg # Ícone Assinatura LTV (documento com selo perpétuo)
|
||||
│ ├── validate.svg # Ícone Validar Assinatura (documento com checkmark verde)
|
||||
│ ├── validate-timestamp.svg # Ícone Validar Carimbo (relógio com checkmark verde)
|
||||
│ ├── exit.svg # Ícone Sair do Agente (porta com seta vermelha de saída)
|
||||
│ ├── logs.svg # Ícone Terminal de Logs (janela com prompt </>)
|
||||
│ ├── settings.svg # Ícone Configurações (engrenagem de precisão de 8 dentes)
|
||||
│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play)
|
||||
│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro)
|
||||
│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i')
|
||||
│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais)
|
||||
│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo)
|
||||
│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna)
|
||||
│ ├── 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
|
||||
│
|
||||
└── 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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Como Testar Suas Modificações
|
||||
|
||||
Para compilar e gerar o pacote `.jar` completo com todos os novos assets embutidos:
|
||||
```bash
|
||||
./build-jar.sh
|
||||
```
|
||||
|
||||
Para rodar a aplicação:
|
||||
```bash
|
||||
java -jar ./cybercore-signer.jar
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 429 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Folha de documento com dobra -->
|
||||
<path d="M 10 4 L 34 4 L 42 12 L 42 44 L 10 44 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<path d="M 34 4 L 34 12 L 42 12 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2"/>
|
||||
<!-- Linhas de texto representativas -->
|
||||
<line x1="16" y1="20" x2="36" y2="20" stroke="#90caf9" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="16" y1="26" x2="36" y2="26" stroke="#90caf9" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="16" y1="32" x2="30" y2="32" stroke="#90caf9" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Clipe de papel metálico prendendo a folha -->
|
||||
<path d="M 12 2 C 8 2 6 5 6 9 L 6 25 C 6 29 9 32 14 32 C 18 32 21 29 21 25 L 21 11 C 21 8 19 6 16 6 C 13 6 12 8 12 11 L 12 24"
|
||||
fill="none" stroke="#455a64" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 976 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Folha traseira (3) -->
|
||||
<path d="M 16 4 L 38 4 L 44 10 L 44 32 L 16 32 Z" fill="#e1f5fe" stroke="#4fc3f7" stroke-width="2" stroke-linejoin="round"/>
|
||||
<!-- Folha do meio (2) -->
|
||||
<path d="M 10 10 L 32 10 L 38 16 L 38 38 L 10 38 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
|
||||
<!-- Folha frontal (1) -->
|
||||
<path d="M 4 16 L 26 16 L 32 22 L 32 44 L 4 44 Z" fill="#ffffff" stroke="#01579b" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
<!-- Dobra folha frontal -->
|
||||
<path d="M 26 16 L 26 22 L 32 22 Z" fill="#e1f5fe" stroke="#01579b" stroke-width="1.8"/>
|
||||
<!-- Checkmark de execução/lote concluído -->
|
||||
<circle cx="34" cy="34" r="10" fill="#00c853"/>
|
||||
<path d="M 29 34 L 33 38 L 39 30" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 920 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Pasta de arquivos de certificados -->
|
||||
<path d="M 4 10 L 16 10 L 20 14 L 44 14 L 44 40 L 4 40 Z" fill="#e1f5fe" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<!-- Aba superior da pasta -->
|
||||
<path d="M 4 18 L 44 18" stroke="#0288d1" stroke-width="2"/>
|
||||
<!-- Cadeado de segurança dourado/azul -->
|
||||
<rect x="26" y="24" width="14" height="12" rx="3" fill="#0288d1"/>
|
||||
<!-- Arco do cadeado -->
|
||||
<path d="M 29 24 L 29 20 C 29 17 31 15 33 15 C 35 15 37 17 37 20 L 37 24" fill="none" stroke="#01579b" stroke-width="2.5"/>
|
||||
<circle cx="33" cy="30" r="1.8" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 688 B |
@@ -0,0 +1,4 @@
|
||||
<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="#10b981"/>
|
||||
<path d="M 14 24 L 21 31 L 34 17" fill="none" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Folha secundária -->
|
||||
<path d="M 6 10 L 26 10 L 32 16 L 32 40 L 6 40 Z" fill="#ffffff" stroke="#1976d2" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
<!-- Folha primária -->
|
||||
<path d="M 16 4 L 36 4 L 42 10 L 42 34 L 16 34 Z" fill="#e3f2fd" stroke="#0288d1" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
<path d="M 36 4 L 36 10 L 42 10 Z" fill="#bbdefb" stroke="#0288d1" stroke-width="1.8"/>
|
||||
<!-- Ícone de dois usuários / parceiros de assinatura -->
|
||||
<circle cx="16" cy="24" r="4" fill="#0d47a1"/>
|
||||
<path d="M 10 36 C 10 31 13 30 16 30 C 19 30 22 31 22 36 Z" fill="#0d47a1"/>
|
||||
<circle cx="26" cy="22" r="3.5" fill="#0288d1"/>
|
||||
<path d="M 21 34 C 21 30 24 29 26 29 C 29 29 32 30 32 34 Z" fill="#0288d1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 815 B |
@@ -0,0 +1,5 @@
|
||||
<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="#ef4444"/>
|
||||
<line x1="16" y1="16" x2="32" y2="32" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
|
||||
<line x1="32" y1="16" x2="16" y2="32" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 343 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Porta de saída / batente -->
|
||||
<path d="M 24 8 L 12 8 C 9.8 8 8 9.8 8 12 L 8 36 C 8 38.2 9.8 40 12 40 L 24 40"
|
||||
fill="none" stroke="#546e7a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- Seta vermelha apontando para fora -->
|
||||
<line x1="18" y1="24" x2="40" y2="24" stroke="#d32f2f" stroke-width="3.2" stroke-linecap="round"/>
|
||||
<polyline points="32 16, 40 24, 32 32" fill="none" stroke="#d32f2f" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 598 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- 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="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<!-- 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="34" y2="20" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="14" y1="26" x2="34" y2="26" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Caneta / Pena de assinatura eletrônica -->
|
||||
<path d="M 16 38 L 30 24 L 35 29 L 21 43 L 14 44 Z" fill="#0288d1"/>
|
||||
<path d="M 30 24 L 34 20 L 39 25 L 35 29 Z" fill="#01579b"/>
|
||||
<circle cx="16" cy="42" r="1.5" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 873 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Janela de terminal / console -->
|
||||
<rect x="4" y="6" width="40" height="36" rx="6" fill="#1e293b" stroke="#334155" stroke-width="2"/>
|
||||
<!-- Barra de título do terminal com botões -->
|
||||
<circle cx="10" cy="12" r="2" fill="#ef4444"/>
|
||||
<circle cx="16" cy="12" r="2" fill="#f59e0b"/>
|
||||
<circle cx="22" cy="12" r="2" fill="#10b981"/>
|
||||
<!-- Linha divisória -->
|
||||
<line x1="4" y1="18" x2="44" y2="18" stroke="#334155" stroke-width="1.5"/>
|
||||
<!-- Texto e prompt de código </> -->
|
||||
<path d="M 12 24 L 18 29 L 12 34" fill="none" stroke="#38bdf8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="22" y1="34" x2="32" y2="34" stroke="#94a3b8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 811 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Folha de documento com dobra -->
|
||||
<path d="M 8 4 L 30 4 L 38 12 L 38 34 L 8 34 Z" fill="#ffffff" stroke="#00838f" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<path d="M 30 4 L 30 12 L 38 12 Z" fill="#b2dfdb" stroke="#00838f" stroke-width="2"/>
|
||||
<!-- Linhas no documento -->
|
||||
<line x1="14" y1="18" x2="30" y2="18" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="14" y1="24" x2="24" y2="24" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Selo LTV com fita de proteção permanente -->
|
||||
<circle cx="32" cy="32" r="12" fill="#e0f2f1" stroke="#00897b" stroke-width="2.5"/>
|
||||
<text x="32" y="36.5" font-family="Segoe UI, Arial, sans-serif" font-size="9" font-weight="bold" fill="#00695c" text-anchor="middle">LTV</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |
@@ -0,0 +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"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 314 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- 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="#d32f2f" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<!-- 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>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 996 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Fita pendente do selo -->
|
||||
<polygon points="18,30 24,44 30,30" fill="#f59e0b"/>
|
||||
<polygon points="12,30 16,42 20,30" fill="#d97706"/>
|
||||
<polygon points="28,30 32,42 36,30" fill="#d97706"/>
|
||||
<!-- Medalha / Selo circular -->
|
||||
<circle cx="24" cy="20" r="16" fill="#d97706" stroke="#fbbf24" stroke-width="2.5"/>
|
||||
<circle cx="24" cy="20" r="13" fill="none" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="2,2"/>
|
||||
<!-- Estrela de 5 pontas central -->
|
||||
<polygon points="24,9 26.5,16.5 34,16.5 28,21 30.5,28.5 24,24 17.5,28.5 20,21 14,16.5 21.5,16.5" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 671 B |
@@ -0,0 +1,8 @@
|
||||
<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"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 817 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Escudo CyberShield -->
|
||||
<path d="M 24 4 L 8 11 L 8 24 C 8 34.5 15 42.5 24 45 C 33 42.5 40 34.5 40 24 L 40 11 Z" fill="#0284c7" stroke="#38bdf8" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<!-- Chave de segurança criptográfica no centro -->
|
||||
<path d="M 24 14 C 20.7 14 18 16.7 18 20 C 18 22.4 19.5 24.5 22 25.4 L 22 34 L 26 34 L 26 30 L 28 30 L 28 27 L 26 27 L 26 25.4 C 28.5 24.5 30 22.4 30 20 C 30 16.7 27.3 14 24 14 Z M 24 18 C 25.1 18 26 18.9 26 20 C 26 21.1 25.1 22 24 22 C 22.9 22 22 21.1 22 20 C 22 18.9 22.9 18 24 18 Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 649 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<polygon points="24,4 29.5,16.5 43,17.5 32.5,26.5 35.8,40 24,32.5 12.2,40 15.5,26.5 5,17.5 18.5,16.5"
|
||||
fill="#f59e0b" stroke="#d97706" stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Núcleo solar brilhante -->
|
||||
<circle cx="24" cy="24" r="10" fill="#f59e0b"/>
|
||||
<!-- Raios solares -->
|
||||
<g stroke="#f59e0b" stroke-width="3" stroke-linecap="round">
|
||||
<line x1="24" y1="4" x2="24" y2="9"/>
|
||||
<line x1="24" y1="39" x2="24" y2="44"/>
|
||||
<line x1="4" y1="24" x2="9" y2="24"/>
|
||||
<line x1="39" y1="24" x2="44" y2="24"/>
|
||||
<line x1="9.8" y1="9.8" x2="13.3" y2="13.3"/>
|
||||
<line x1="34.7" y1="34.7" x2="38.2" y2="38.2"/>
|
||||
<line x1="9.8" y1="38.2" x2="13.3" y2="34.7"/>
|
||||
<line x1="34.7" y1="13.3" x2="38.2" y2="9.8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 647 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Folha de documento com dobra -->
|
||||
<path d="M 8 4 L 30 4 L 38 12 L 38 34 L 8 34 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<path d="M 30 4 L 30 12 L 38 12 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2"/>
|
||||
<!-- Linhas no documento -->
|
||||
<line x1="14" y1="18" x2="30" y2="18" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="14" y1="24" x2="24" y2="24" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Relógio de precisão temporal TSA -->
|
||||
<circle cx="32" cy="32" r="12" fill="#e1f5fe" stroke="#0288d1" stroke-width="2.5"/>
|
||||
<circle cx="32" cy="32" r="2" fill="#01579b"/>
|
||||
<line x1="32" y1="24" x2="32" y2="32" stroke="#01579b" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="32" y1="32" x2="38" y2="32" stroke="#01579b" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 958 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Monitor de computador -->
|
||||
<rect x="4" y="6" width="40" height="28" rx="4" fill="#ffffff" stroke="#0288d1" stroke-width="2.5"/>
|
||||
<!-- Base do monitor -->
|
||||
<path d="M 16 34 L 12 42 L 36 42 L 32 34" fill="none" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<!-- Ícone de play/vídeo tutorial central -->
|
||||
<polygon points="20,14 32,20 20,26" fill="#0288d1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- Círculo de relógio base -->
|
||||
<circle cx="24" cy="24" r="20" fill="#f1f8e9" stroke="#558b2f" stroke-width="2.5"/>
|
||||
<!-- Ponteiros do relógio -->
|
||||
<line x1="24" y1="12" x2="24" y2="24" stroke="#33691e" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="24" y1="24" x2="32" y2="24" stroke="#33691e" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Checkmark verde de validação de tempo -->
|
||||
<circle cx="34" cy="34" r="10" fill="#2e7d32"/>
|
||||
<path d="M 29 34 L 33 38 L 39 30" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 679 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- 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="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2"/>
|
||||
<!-- Linhas no documento -->
|
||||
<line x1="14" y1="20" x2="34" y2="20" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="14" y1="26" x2="28" y2="26" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Emblema de validação com Checkmark verde -->
|
||||
<circle cx="30" cy="34" r="11" fill="#e8f5e9" stroke="#2e7d32" stroke-width="2.5"/>
|
||||
<path d="M 24 34 L 28 38 L 36 29" fill="none" stroke="#2e7d32" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 848 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<!-- 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="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<!-- 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"><XML></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 998 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# CyberCore Signer - Script de Automação de Build do Fat JAR Executável
|
||||
# ==============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# Garante execução no diretório raiz do projeto
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "${SCRIPT_DIR}"
|
||||
|
||||
COLOR_RESET="\033[0m"
|
||||
COLOR_BOLD="\033[1m"
|
||||
COLOR_CYAN="\033[36m"
|
||||
COLOR_GREEN="\033[32m"
|
||||
COLOR_YELLOW="\033[33m"
|
||||
COLOR_RED="\033[31m"
|
||||
|
||||
RUN_AFTER_BUILD=false
|
||||
RUN_TESTS=false
|
||||
|
||||
# Interpretação de argumentos
|
||||
for arg in "$@"; do
|
||||
case "${arg}" in
|
||||
--with-tests|-t)
|
||||
RUN_TESTS=true
|
||||
;;
|
||||
--run|-r)
|
||||
RUN_AFTER_BUILD=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo -e "${COLOR_BOLD}Uso: ./build-jar.sh [opções]${COLOR_RESET}"
|
||||
echo ""
|
||||
echo "Opções disponíveis:"
|
||||
echo " --with-tests, -t Executa a suíte completa de testes unitários antes de empacotar."
|
||||
echo " --run, -r Inicia automaticamente o CyberCore Signer após a compilação."
|
||||
echo " --help, -h Exibe esta mensagem de ajuda."
|
||||
echo ""
|
||||
echo "Exemplo:"
|
||||
echo " ./build-jar.sh # Compilação e empacotamento rápido do JAR"
|
||||
echo " ./build-jar.sh --run # Compila e já abre a aplicação"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${COLOR_YELLOW}Aviso: Opção desconhecida: ${arg}. Use --help para detalhes.${COLOR_RESET}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${COLOR_CYAN}${COLOR_BOLD}"
|
||||
echo "============================================================"
|
||||
echo " CYBERCORE SIGNER - AUTOMAÇÃO DE BUILD DO JAR"
|
||||
echo "============================================================"
|
||||
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/claudecio/.jdks/jdk-25.0.4.1+1"
|
||||
"/usr/lib/jvm/java-25-openjdk-amd64"
|
||||
"/usr/lib/jvm/java-21-openjdk-amd64"
|
||||
"/usr/lib/jvm/default-java"
|
||||
)
|
||||
for candidate in "${POSSIBLE_JDKS[@]}"; do
|
||||
if [ -x "${candidate}/bin/java" ]; then
|
||||
export JAVA_HOME="${candidate}"
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ! command -v java &>/dev/null; then
|
||||
echo -e "${COLOR_RED}Erro: Java não foi encontrado no PATH nem nos diretórios padrão de JDK.${COLOR_RESET}"
|
||||
echo -e "${COLOR_YELLOW}Dica: Não é necessário executar com 'sudo'. Tente executar normalmente: ./build-jar.sh${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_VERSION=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | head -n 1)
|
||||
echo -e "Ambiente Java detectado: ${COLOR_BOLD}${JAVA_VERSION}${COLOR_RESET} (${JAVA_HOME:-$(which java)})"
|
||||
|
||||
# 2. Verificação do Maven Wrapper
|
||||
if [[ ! -f "./mvnw" ]]; then
|
||||
echo -e "${COLOR_RED}Erro: Maven Wrapper (./mvnw) não encontrado na raiz do projeto.${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x ./mvnw
|
||||
|
||||
# 3. Execução do Build
|
||||
MVN_ARGS=("clean" "package")
|
||||
if [[ "${RUN_TESTS}" == "false" ]]; then
|
||||
echo -e "${COLOR_YELLOW}Modo rápido ativado (pulando testes unitários). Para testar use: ./build-jar.sh --with-tests${COLOR_RESET}"
|
||||
MVN_ARGS+=("-DskipTests")
|
||||
else
|
||||
echo -e "${COLOR_CYAN}Executando testes unitários e de integração...${COLOR_RESET}"
|
||||
fi
|
||||
|
||||
echo -e "Iniciando compilação e geração do Fat JAR via Maven..."
|
||||
./mvnw "${MVN_ARGS[@]}"
|
||||
|
||||
SOURCE_JAR="modules/cybercore-app/target/cybercore-signer.jar"
|
||||
DEST_JAR="./cybercore-signer.jar"
|
||||
|
||||
if [[ ! -f "${SOURCE_JAR}" ]]; then
|
||||
echo -e "${COLOR_RED}Erro: O artefato ${SOURCE_JAR} não foi gerado pelo build.${COLOR_RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Copia para a raiz para facilitar o acesso
|
||||
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
|
||||
|
||||
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
|
||||
|
||||
echo ""
|
||||
echo -e "${COLOR_GREEN}${COLOR_BOLD}============================================================${COLOR_RESET}"
|
||||
echo -e "${COLOR_GREEN}${COLOR_BOLD}✓ JAR GERADO COM SUCESSO!${COLOR_RESET}"
|
||||
echo -e "Arquivo: ${COLOR_BOLD}${DEST_JAR}${COLOR_RESET} (${JAR_SIZE})"
|
||||
echo -e "Local de origem: ${SOURCE_JAR}"
|
||||
echo -e "${COLOR_GREEN}${COLOR_BOLD}============================================================${COLOR_RESET}"
|
||||
echo ""
|
||||
echo -e "Para testar manualmente, execute:"
|
||||
echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR}${COLOR_RESET} # Interface gráfica desktop"
|
||||
echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR} --dev${COLOR_RESET} # Modo desenvolvimento (logs detalhados)"
|
||||
echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR} --headless${COLOR_RESET} # Modo console de segundo plano (porta 21325)"
|
||||
echo ""
|
||||
|
||||
# 5. Execução automática opcional
|
||||
if [[ "${RUN_AFTER_BUILD}" == "true" ]]; then
|
||||
echo -e "${COLOR_BOLD}Iniciando aplicação agora...${COLOR_RESET}"
|
||||
java -jar "${DEST_JAR}"
|
||||
fi
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Exclusões do SpotBugs / FindSecBugs.
|
||||
Regra: toda exclusão deve ser específica (classe/padrão) e justificada em comentário.
|
||||
-->
|
||||
<FindBugsFilter
|
||||
xmlns="https://github.com/spotbugs/filter/3.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
|
||||
|
||||
<!--
|
||||
Records imutáveis de DTO e relatório que já realizam cópia defensiva em seus
|
||||
construtores canônicos (List.copyOf / Map.copyOf) mas são apontados por heurística do SpotBugs.
|
||||
-->
|
||||
<Match>
|
||||
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
|
||||
<Or>
|
||||
<Class name="br.com.cybercore.signer.core.diagnostics.DiagnosticReport"/>
|
||||
<Class name="br.com.cybercore.signer.core.domain.SigningResult"/>
|
||||
<Class name="br.com.cybercore.signer.core.protocol.CcspResponse"/>
|
||||
</Or>
|
||||
</Match>
|
||||
</FindBugsFilter>
|
||||
@@ -0,0 +1,208 @@
|
||||
# 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**.
|
||||
|
||||
A versão inicial descrita neste documento é a **CCSP/1**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Princípios de Segurança
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 2. Endpoints do Protocolo (CCSP/1)
|
||||
|
||||
### 2.1 `GET /ccsp/v1/status`
|
||||
Verifica se o agente está instalado, em execução e pronto para operações.
|
||||
|
||||
#### Resposta de Sucesso (200 OK)
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"status": "ready",
|
||||
"agent": "CyberCore Signer",
|
||||
"agent_version": "0.1.0",
|
||||
"os": "Windows 11",
|
||||
"timestamp": "2026-10-08T19:40:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `GET /ccsp/v1/certificates`
|
||||
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura.
|
||||
|
||||
#### Headers / Parâmetros
|
||||
- `X-CyberCore-AppId`: UUID da aplicação autorizada (ou query param `?application_id=uuid`).
|
||||
- `Origin`: Origem web autorizada.
|
||||
|
||||
#### Resposta de Sucesso (200 OK)
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"status": "success",
|
||||
"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",
|
||||
"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",
|
||||
"serial_number": "72910482910482019482",
|
||||
"can_sign": true,
|
||||
"status": "AVAILABLE"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 `POST /ccsp/v1/authorize`
|
||||
Handshake inicial de uma nova aplicação web para solicitar autorização ao usuário.
|
||||
|
||||
#### Requisição
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Portal Assinaturas Jurídicas"
|
||||
}
|
||||
```
|
||||
|
||||
#### Resposta (200 OK)
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"status": "authorized",
|
||||
"application_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 `POST /ccsp/v1/sign`
|
||||
Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquivos gerais).
|
||||
|
||||
#### Requisição
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"operation": "sign",
|
||||
"algorithm": "SHA256withRSA",
|
||||
"digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
||||
"certificate_hint": "win-4340656a",
|
||||
"document_name": "contrato_locacao.pdf",
|
||||
"document_type": "PDF",
|
||||
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
||||
"expires_at": "2026-10-08T19:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Resposta de Sucesso (200 OK)
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||
"status": "signed",
|
||||
"signature": "MEUCIQDq...base64...",
|
||||
"certificate": "MIIFazC...base64...",
|
||||
"certificate_chain": [
|
||||
"MIIFazC...base64...",
|
||||
"MIIEazC...base64..."
|
||||
],
|
||||
"algorithm": "SHA256withRSA",
|
||||
"subject_name": "CLAUDIO SILVA",
|
||||
"subject_cpf_cnpj": "12345678900",
|
||||
"document_name": "contrato_locacao.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).
|
||||
|
||||
#### Requisição
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"operation": "authenticate",
|
||||
"challenge": "server-session-challenge-nonce-2026",
|
||||
"certificate_hint": "win-4340656a",
|
||||
"purpose": "AUTHENTICATION",
|
||||
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
||||
"expires_at": "2026-10-08T19:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Resposta de Sucesso (200 OK)
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||
"status": "authenticated",
|
||||
"signature": "MEUCIQDq...base64...",
|
||||
"certificate": "MIIFazC...base64...",
|
||||
"certificate_chain": [
|
||||
"MIIFazC...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."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Tabela de Códigos de Erro Estruturados
|
||||
|
||||
| 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. |
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Certificados Digitais A1 (Software / PKCS#12)
|
||||
|
||||
Os certificados A1 são arquivos com extensão `.pfx` ou `.p12` protegidos por senha.
|
||||
|
||||
### 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")`.
|
||||
|
||||
---
|
||||
|
||||
## 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`).
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## 3. Hardware PKCS#11 Direto (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`).
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão Geral da Arquitetura em Módulos
|
||||
|
||||
O projeto adota uma arquitetura desacoplada em múltiplos módulos Maven independentes:
|
||||
|
||||
```
|
||||
cybercore-signer/
|
||||
├── modules/
|
||||
│ ├── cybercore-core/ # Núcleo de criptografia, provedores, protocolo e segurança
|
||||
│ ├── cybercore-ui/ # Interface gráfica JavaFX, diálogos nativos e System Tray
|
||||
│ └── cybercore-app/ # Ponto de entrada (Main), bootstrap e empacotamento nativo
|
||||
├── sdk/ # SDK Web em JavaScript (@cybercore/signer-sdk)
|
||||
├── scripts/ # Scripts de build nativo jpackage para Windows e Linux
|
||||
└── 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.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.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`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Fluxo de Dados e Princípio de Menor Privilégio
|
||||
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor Usuario as Usuário
|
||||
participant Web as Aplicação Web (SDK)
|
||||
participant CCSP as CcspServer (127.0.0.1)
|
||||
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
|
||||
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-->>CCSP: Confirmação concedida
|
||||
CCSP->>Crypto: sign(SigningContext) [Trava FIFO]
|
||||
Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign()
|
||||
Crypto-->>CCSP: SigningResult (signatureBytes, certChain)
|
||||
CCSP-->>Web: JSON { status: "signed", signature: "...", certificate: "..." }
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# Autenticação e Login Digital via CyberCore Signer
|
||||
|
||||
Este documento descreve o fluxo de **Login e Autenticação de Usuários com Certificado Digital ICP-Brasil (A1 e A3)** através do agente desktop **CyberCore Signer** e o protocolo **CCSP/1**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão Geral do Modelo de Autenticação
|
||||
|
||||
O login por certificado digital no CyberCore Signer adota o padrão de mercado **Challenge-Response Criptográfico**:
|
||||
1. O backend da sua aplicação web gera um **desafio único (challenge / nonce)** para a sessão atual.
|
||||
2. A aplicação web solicita a assinatura desse desafio ao **CyberCore Signer** através do SDK JavaScript.
|
||||
3. O CyberCore Signer apresenta ao usuário na tela nativa do computador uma solicitação visual com destaque de **LOGIN / AUTENTICAÇÃO**:
|
||||
- Identifica a aplicação solicitante e a origem;
|
||||
- Mostra o certificado selecionado e o titular;
|
||||
- Solicita aprovação expressa do usuário (e a digitação do PIN caso seja token ou smart card A3).
|
||||
4. O CyberCore assina o desafio com a chave privada do certificado local e devolve:
|
||||
- A **assinatura digital do desafio**;
|
||||
- O **certificado X.509 completo** (DER Base64) e cadeia;
|
||||
- O **nome e CPF/CNPJ** do titular extraídos do certificado.
|
||||
5. O frontend web envia a resposta de volta ao backend, que:
|
||||
- Valida criptograficamente a assinatura com a chave pública do certificado recebido;
|
||||
- Confirma a validade e cadeia do certificado junto à ICP-Brasil;
|
||||
- Localiza o usuário correspondente no banco de dados pelo CPF/CNPJ;
|
||||
- Cria a sessão autenticada do usuário no sistema.
|
||||
|
||||
---
|
||||
|
||||
## 2. Exemplo de Integração Frontend com o SDK
|
||||
|
||||
```javascript
|
||||
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||
|
||||
const signer = new CyberCoreSigner({
|
||||
applicationName: 'Portal Corporativo'
|
||||
});
|
||||
|
||||
async function realizarLoginComCertificado(certificateId) {
|
||||
// 1. Obter desafio único gerado pelo seu backend
|
||||
const { challenge } = await fetch('/api/auth/challenge').then(r => r.json());
|
||||
|
||||
// 2. Solicitar autenticação ao CyberCore Signer
|
||||
const authResult = await signer.authenticate({
|
||||
certificateId: certificateId,
|
||||
challenge: challenge
|
||||
});
|
||||
|
||||
// 3. Enviar assinatura e certificado ao backend para validação e abertura de sessão
|
||||
const sessionResponse = await fetch('/api/auth/login-certificate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
challenge: authResult.challenge,
|
||||
signature: authResult.signature,
|
||||
certificate: authResult.certificate
|
||||
})
|
||||
});
|
||||
|
||||
if (sessionResponse.ok) {
|
||||
console.log('Usuário autenticado com sucesso:', authResult.subjectName);
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Segurança e Prevenção de Ataques
|
||||
|
||||
- **Prevenção de Ataques de Replay:** Cada desafio deve possuir tempo de expiração curto (ex: 60 a 90 segundos) e ser descartado no servidor após uma única tentativa.
|
||||
- **Proteção da Chave Privada:** A chave privada e o PIN do usuário permanecem exclusivamente sob custódia do hardware/computador local do usuário.
|
||||
- **Consentimento Explícito:** O CyberCore Signer nunca assina desafios em segundo plano sem a exibição do diálogo e autorização do usuário na tela nativa.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Modo de Desenvolvimento e Diagnóstico Detalhado
|
||||
|
||||
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**.
|
||||
|
||||
---
|
||||
|
||||
## 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`:
|
||||
|
||||
```bash
|
||||
java -jar cybercore-signer.jar --dev
|
||||
```
|
||||
|
||||
Ao iniciar, uma mensagem informativa é registrada no log:
|
||||
```
|
||||
=========================================================
|
||||
MODO DE DESENVOLVIMENTO ATIVADO (--dev)
|
||||
Logs detalhados ativos. Operação com certificados reais.
|
||||
=========================================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Testando em Ambiente de Desenvolvimento
|
||||
|
||||
Para desenvolvedores testarem fluxos completos de assinatura de PDF, XML, arquivos em geral e login digital:
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 3. Garantia de Operação Real
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Diagnóstico de Ambiente e Auto-Teste
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Itens Inspecionados no Diagnóstico
|
||||
|
||||
Ao abrir a aba **Diagnóstico** na interface ou invocar `diagnosticService.runDiagnostic()`, o agente avalia:
|
||||
|
||||
1. **Ambiente Java / Runtime:**
|
||||
- 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.
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Tabela de Erros Estruturados
|
||||
|
||||
| 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. |
|
||||
|
After Width: | Height: | Size: 147 KiB |
@@ -0,0 +1,43 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage`
|
||||
|
||||
O empacotamento utiliza a ferramenta nativa `jpackage` incluída no JDK 21/25.
|
||||
|
||||
### 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:
|
||||
```cmd
|
||||
mvnw clean package -DskipTests
|
||||
```
|
||||
2. Invoca o `jpackage` criando o instalador Windows:
|
||||
- Extrai um runtime mínimo customizado contendo apenas os módulos necessários (`java.base`, `java.desktop`, `java.smartcardio`, `jdk.crypto.cryptoki`, etc.).
|
||||
- Cria atalhos na Área de Trabalho e no Menu Iniciar.
|
||||
- Gera um instalador padrão MSI ou EXE em `dist/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Instalação e Inicialização com o Sistema
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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:
|
||||
|
||||
---
|
||||
|
||||
## 1. Dependências de Sistema Operacional e Hardware
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 2. Limitações de Rede e Concorrência
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Assinaturas PDF (PAdES) e Selos Visuais em SVG
|
||||
|
||||
Este documento descreve a arquitetura e o funcionamento da assinatura digital de arquivos PDF no padrão **PAdES (PDF Advanced Electronic Signatures)** no CyberCore Signer, incluindo o subsistema de **selos visuais temáticos baseados em SVG**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão Geral da Arquitetura
|
||||
|
||||
O CyberCore Signer implementa assinatura PAdES em dois modos complementares:
|
||||
|
||||
1. **Modo Desacoplado via Protocolo CCSP/1 (Web):** A aplicação web prepara a estrutura `/ByteRange`, envia apenas o digest SHA-256 e o CyberCore assina localmente com o certificado A1/A3 sem expor a chave privada.
|
||||
2. **Modo Desktop Completo (Desktop UI):** O usuário seleciona um arquivo PDF localmente, escolhe o certificado ICP-Brasil, customiza o selo visual e o `PdfSigningService` gera o PDF assinado completo com envelope CMS/PKCS#7 e estampa gráfica.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Arquivo PDF Original] --> B[PdfSigningService]
|
||||
C[Tema de Selo SVG] --> D[PdfSealRenderer Java2D]
|
||||
E[Metadados Signatário] --> D
|
||||
D -->|Estampa Visual 300 DPI| B
|
||||
F[Chave Privada A1/A3] --> G[Bouncy Castle CMS Generator]
|
||||
B -->|PAdES Incremental| H[PDF Assinado Digitalmente]
|
||||
G --> H
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Subsistema de Selos Visuais em SVG e Temas
|
||||
|
||||
O selo visual é uma representação gráfica da assinatura estipulada em conformidade com as recomendações do ITI e MP nº 2.200-2/2001.
|
||||
|
||||
### 2.1 Temas Pré-configurados e Ícones Vetoriais (`PdfSealThemeRegistry` & `PdfSealRenderer`)
|
||||
|
||||
Os brasões e ícones padrão do sistema ficam centralizados em:
|
||||
- **Catálogo de Temas:** `modules/cybercore-core/.../pdf/seal/PdfSealThemeRegistry.java` (definição de cores, dimensões, textos padrão e tipo de ícone).
|
||||
- **Desenho Vetorial Java2D:** `modules/cybercore-core/.../pdf/seal/PdfSealRenderer.drawThemeIcon()` (renderização matemática em Java2D dos brasões padrão: `shield`, `star`, `crystal`, `notary`, etc.).
|
||||
|
||||
| ID do Tema | Nome | Descrição | Ícone Vetorial |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `icp-brasil` | **ICP-Brasil Oficial** | Padrão clássico com brasão e cadeado verde/azul | Escudo e cadeado |
|
||||
| `cybercore-modern` | **CyberCore Modern** | Estilo tecnológico neon com cristal multifacetado | Cristal vetorial |
|
||||
| `minimal-clean` | **Minimalista Clean** | Monocromático para relatórios executivos sóbrios | Carimbo notarial com check |
|
||||
| `gov-br` | **Gov.br Oficial** | Identidade visual Verde-Oliva conforme MP 2.200-2/2001 | Brasão estelar estilizado |
|
||||
|
||||
### 2.2 Renderização Gráfica (`PdfSealRenderer`)
|
||||
|
||||
- O núcleo `cybercore-core` **não depende de JavaFX**, atendendo às regras estritas de arquitetura (`ArchitectureTest`).
|
||||
- A renderização é efetuada através da API **Java2D (AWT)** nativa com anti-aliasing duplo (`RenderingHints.VALUE_ANTIALIAS_ON`, subpixel text, escala 2x / 300 DPI).
|
||||
- Exporta tanto `BufferedImage` quanto `byte[]` PNG para inclusão no PDFBox ou pré-visualização dinâmica na interface com `ImageView`.
|
||||
|
||||
### 2.3 Criação, Instalação e Privacidade de Selos Personalizados
|
||||
|
||||
- **Foco Exclusivo no Titular (Sem Emissor/AC no Gráfico):** O selo foi desenhado para ser limpo e elegante. Dados técnicos do certificado e o emissor da Autoridade Certificadora não são exibidos no gráfico do selo, evitando poluição visual.
|
||||
- **Obrigatoriedade do Nome Completo:** O renderizador calcula dinamicamente a métrica de fontes via `drawFittedText()` para garantir que o nome completo do signatário seja sempre exibido na íntegra, com destaque tipográfico (13.5pt a 14.5pt em negrito), sem cortes ou truncamentos.
|
||||
- **Upload de Logotipo / Emblema Próprio:** Suporte a upload de imagens de logotipos (PNG, JPG, BMP) armazenadas de forma portável em Base64 dentro do tema, com renderização redimensionada e anti-aliasing de 300 DPI.
|
||||
- **Campos Customizáveis (Matrícula, Cargo, OAB, etc.):** Permite definir rótulo e valor padrão no tema (ex: `Matrícula: 104829/SP` ou `Cargo: Auditor Fiscal`) ou informar pontualmente durante a assinatura via `customExtraInfo`. A informação é posicionada de forma harmônica na linha de identificação do signatário.
|
||||
- **Privacidade do CPF (LGPD):** O modelo `CpfDisplayMode` suporta três modalidades:
|
||||
- `FULL`: exibe o documento completo (`CPF: 123.456.789-00`).
|
||||
- `MASKED`: mascara dígitos sensíveis (`CPF: ***.456.789-**`).
|
||||
- `NONE`: oculta totalmente o CPF do gráfico do selo.
|
||||
- **Data e Hora Opcional:** Parâmetro booleano para incluir ou suprimir data/hora no gráfico.
|
||||
- **Instalação e Persistência Local:** Através de `CustomSealStorage`, novos selos criados ou importados são salvos em `~/.cybercore/seals/*.json` e registrados dinamicamente no `PdfSealThemeRegistry`.
|
||||
|
||||
### 2.4 Gerenciamento Centralizado de Selos (`SealManagementDialog`)
|
||||
|
||||
- **Lista Completa:** Exibição unificada de selos embutidos do sistema e selos personalizados instalados pelo usuário.
|
||||
- **Selo Padrão Ativo:** Permite definir qual selo é o padrão ativo para todas as assinaturas do usuário, gravado em `AgentConfiguration` (`defaultSealThemeId`).
|
||||
- **Pré-visualização em Tempo Real:** Renderização fiel do selo com dados de exemplo ao navegar pela lista.
|
||||
- **Clonagem e Edição:** Permite duplicar qualquer selo (inclusive os embutidos) e abri-lo no `SealDesignerDialog` para customização.
|
||||
- **Importação e Exportação:** Exportação de temas em arquivos `.json` e importação com um clique para compartilhamento entre máquinas e equipes.
|
||||
|
||||
---
|
||||
|
||||
## 3. Posicionamento e Pré-Visualização Interativa
|
||||
|
||||
A posição e a página da estampa são parametrizáveis via `PdfSignatureOptions`:
|
||||
|
||||
- **Posições Automáticas:** `BOTTOM_RIGHT`, `BOTTOM_LEFT`, `TOP_RIGHT`, `TOP_LEFT`.
|
||||
- **Posição Interativa Personalizada (`CUSTOM`):** O usuário clica ou arrasta o selo diretamente sobre a página do PDF na interface gráfica. As coordenadas na tela são matematicamente mapeadas para as coordenadas de mídia do PDFBox:
|
||||
$$X_{pdf} = \frac{X_{clique}}{Largura_{view}} \times Largura_{pdf}$$
|
||||
$$Y_{pdf} = Altura_{pdf} - \left(\frac{Y_{clique}}{Altura_{view}} \times Altura_{pdf}\right) - Altura_{selo}$$
|
||||
- **Páginas:** `LAST_PAGE` (recomendado), `FIRST_PAGE`, `ALL_PAGES`, ou página específica numérica (`targetPageNumber`).
|
||||
- **Dimensões padrão:** 200 pt de largura por 55 pt de altura, com margens de segurança e clamping contra extravasamento.
|
||||
- **Renderização da Folha:** O `PdfSigningService` fornece `renderPagePngBytes(pdfBytes, pageIndex, dpi)` via PDFBox `PDFRenderer`, permitindo que o diálogo da UI JavaFX renderize a folha de papel em background thread com paginação e sobreposição visual do selo.
|
||||
|
||||
---
|
||||
|
||||
## 4. Estrutura Criptográfica PAdES
|
||||
|
||||
- **Motor:** Apache PDFBox 3.0.4 + Bouncy Castle 1.80 (`bcpkix-jdk18on`).
|
||||
- **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.
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,117 @@
|
||||
# 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**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Instalação e Importação
|
||||
|
||||
### Via Módulo ES6 (Browser ou Bundler Vite/Webpack/Next.js)
|
||||
```javascript
|
||||
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||
|
||||
const signer = new CyberCoreSigner({
|
||||
applicationName: 'Portal Meu Negócio'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Métodos da API
|
||||
|
||||
### `isInstalled(timeoutMs = 2000): Promise<boolean>`
|
||||
Testa rapidamente se o agente desktop está ativo na porta local `21325`.
|
||||
|
||||
### `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.
|
||||
|
||||
```javascript
|
||||
const certs = await signer.getCertificates();
|
||||
certs.forEach(cert => {
|
||||
console.log(`[${cert.type}] ${cert.common_name} (Validade: ${cert.valid_to})`);
|
||||
});
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
```javascript
|
||||
const pdfResult = await signer.signPdf(fileInput.files[0], {
|
||||
certificateId: certs[0].id
|
||||
});
|
||||
```
|
||||
|
||||
### `signXml(xmlContent, options): Promise<Object>`
|
||||
Assina um documento XML (XMLDSig / NF-e). Aceita string com o XML formatado ou objeto de arquivo `File`/`Blob`.
|
||||
|
||||
```javascript
|
||||
const xmlResult = await signer.signXml(xmlString, {
|
||||
certificateId: certs[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.
|
||||
|
||||
```javascript
|
||||
const fileResult = await signer.signFile(fileInput.files[0], {
|
||||
certificateId: certs[0].id
|
||||
});
|
||||
```
|
||||
|
||||
### `authenticate(options): Promise<Object>` (ou `login(options)`)
|
||||
Executa login seguro com certificado digital via desafio criptográfico (Challenge-Response).
|
||||
|
||||
```javascript
|
||||
const auth = await signer.authenticate({
|
||||
certificateId: certs[0].id,
|
||||
challenge: 'server-nonce-12345'
|
||||
});
|
||||
|
||||
console.log('Bem-vindo,', auth.subjectName);
|
||||
console.log('CPF/CNPJ:', auth.subjectCpfCnpj);
|
||||
console.log('Assinatura do Desafio:', auth.signature);
|
||||
```
|
||||
|
||||
### `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`:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
UserCancelledError,
|
||||
CertificateNotFoundError,
|
||||
PinIncorrectError,
|
||||
TokenNotFoundError
|
||||
} 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.');
|
||||
} else {
|
||||
console.error('Erro na assinatura:', err);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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*.
|
||||
|
||||
---
|
||||
|
||||
## 1. Isolamento de Rede
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Layout CyberCore Signer — Design Dual-Theme & Assinatura em Lote
|
||||
|
||||
O **CyberCore Signer** adota uma interface gráfica que une a ergonomia de hub consolidada no mercado nacional com uma identidade visual moderna, autoral e com suporte dinâmico a **Tema Claro (Light)** e **Tema Escuro (Dark)**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Identidade Visual e Filosofia de Design
|
||||
|
||||
Em conformidade com os requisitos do projeto, a interface **não é uma cópia datada**, mas sim uma evolução:
|
||||
1. **Identidade CyberCore:** Logotipo central em cristal tridimensional e escudo criptográfico vetorial (`SvgIcons`), subtítulo "Agente Criptográfico Local • ICP-Brasil" e indicador de segurança em tempo real.
|
||||
2. **Badge de Status Ativo:** Exibe o status da porta local (`● Agente Ativo • 127.0.0.1:21325`) com verde esmeralda no modo claro e verde neon no modo escuro.
|
||||
3. **Alternador Dinâmico de Temas:** Botão pílula localizado no cabeçalho superior que permite ao usuário alternar instantaneamente entre **Modo Claro** e **Modo Escuro**:
|
||||
- Ícone do Sol ☀️ para comutar para o tema Claro.
|
||||
- Ícone da Lua 🌙 para comutar para o tema Escuro.
|
||||
- Classe gerenciadora centralizada: [`ThemeManager.java`](file:///var/www/cybercore/cybercore-signer/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java), que replica a escolha de tema para todos os diálogos secundários.
|
||||
|
||||
---
|
||||
|
||||
## 2. Paleta de Cores dos Temas
|
||||
|
||||
| Componente | Tema Claro (Light) | Tema Escuro (Dark) |
|
||||
|---|---|---|
|
||||
| **Fundo da Janela** | `#f1f5f9` (Slate suave) | `#0b0f17` (Dark profundo) |
|
||||
| **Header Superior** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` |
|
||||
| **Cartões de Ação** | `#ffffff`, borda `#e2e8f0`, hover `#0284c7` | `#131c2e`, borda `#1e293b`, hover `#38bdf8` |
|
||||
| **Texto dos Botões** | `#1e293b` (Cinza escuro de alto contraste) | `#f8fafc` (Branco gelo nítido) |
|
||||
| **Painel Lateral** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` |
|
||||
| **Badge de Status** | Fundo `#ecfdf5`, texto `#047857`, borda `#10b981` | Fundo `rgba(16, 185, 129, 0.15)`, texto `#34d399` |
|
||||
| **Alternador de Tema** | Fundo `#ffffff`, borda `#cbd5e1`, hover `#0284c7` | Fundo `#1e293b`, borda `#334155`, hover `#38bdf8` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Grade Central de Ações (Hub 4x3)
|
||||
|
||||
A interface principal organiza as operações em 12 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. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Painel Lateral de Utilitários
|
||||
|
||||
Na coluna lateral direita, o usuário dispõe de atalhos rápidos para operações do sistema:
|
||||
|
||||
1. **`</> Logs`:** Visualizador de auditoria em tempo real, monitorando requisições locais CCSP/1 e eventos criptográficos.
|
||||
2. **`⚙ Configurações`:** Gerenciador de origens web autorizadas, revogação de acessos e opções de PIN.
|
||||
3. **`💻 Tutorial`:** Guia passo a passo interativo de uso do assinador e integração web.
|
||||
4. **`🗂 Certificados`:** Lista de certificados ICP-Brasil instalados (Windows-MY, A1 e A3) e ferramenta para importação de certificados A1 (`.pfx` / `.p12`).
|
||||
5. **`ℹ Sobre`:** Informações de versão, engine JCA/JCE e garantias de privacidade local.
|
||||
|
||||
---
|
||||
|
||||
## 5. Assinatura Digital em Lote (`BatchSigningDialog`)
|
||||
|
||||
A funcionalidade de assinatura em lote permite:
|
||||
- Seleção de múltiplos arquivos simultâneos via diálogo de arquivos ou pastas.
|
||||
- Visualização de tabela com nome, tamanho e status de cada item em tempo real.
|
||||
- Seleção do certificado digital ICP-Brasil desejado (com exibição de titular, CPF/CNPJ, tipo A1/A3 e validade).
|
||||
- Escolha da pasta de destino para os arquivos assinados (salvando arquivos `.p7s` correspondentes).
|
||||
- Execução assíncrona em thread isolada para manter a interface responsiva durante todo o lote.
|
||||
- Compatibilidade automática com o tema ativo (Claro ou Escuro).
|
||||
|
||||
---
|
||||
|
||||
## 6. Ícones Vetoriais SVG Nativos (`SvgIcons`)
|
||||
|
||||
Todos os ícones e o logotipo central em cristal foram construídos via `SVGPath` e primitivas vetoriais JavaFX nativas:
|
||||
- **Resolução Infinita:** Perfeita nitidez em monitores Full HD, 2K, 4K e configurações de escala de DPI do Windows (100%, 125%, 150%, 200%).
|
||||
- **Sem Dependência de Imagens Rasterizadas:** Sem risco de imagens ausentes ou corrompidas no runtime empacotado.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Assinaturas XML (XMLDSig 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.
|
||||
|
||||
---
|
||||
|
||||
## 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`).
|
||||
|
||||
### 1.1 Casos de Uso Suportados
|
||||
|
||||
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato 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...">`
|
||||
- **MDF-e (Manifesto Eletrônico de Documentos Fiscais):** Assinatura da tag `<infMDFe Id="MDFe...">`
|
||||
- **eSocial / EFD-Reinf:** Assinatura de eventos com tags `<evt... Id="...">`
|
||||
|
||||
---
|
||||
|
||||
## 2. Padrões Criptográficos
|
||||
|
||||
- **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>`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Segurança e Mitigação contra XXE
|
||||
|
||||
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
|
||||
|
||||
```java
|
||||
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);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Validação Pós-Assinatura
|
||||
|
||||
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando:
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>br.com.cybercore.signer</groupId>
|
||||
<artifactId>cybercore-signer-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>cybercore-app</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>CyberCore Signer :: Application</name>
|
||||
<description>Módulo executável desktop, ciclo de vida da aplicação, shutdown hooks e empacotamento nativo.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>br.com.cybercore.signer</groupId>
|
||||
<artifactId>cybercore-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>br.com.cybercore.signer</groupId>
|
||||
<artifactId>cybercore-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFX Neutro -->
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFX Nativos Windows (para execução direta em ambiente Windows) -->
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<classifier>win</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<classifier>win</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<classifier>win</classifier>
|
||||
</dependency>
|
||||
|
||||
<!-- JavaFX Nativos Linux (para execução direta em ambiente Linux) -->
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<classifier>linux</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<classifier>linux</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
<classifier>linux</classifier>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Testes de Arquitetura -->
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>br.com.cybercore.signer.app.CyberCoreSignerApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Geração de Fat JAR executável autocontido -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>cybercore-signer</finalName>
|
||||
<shadedArtifactAttached>false</shadedArtifactAttached>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>br.com.cybercore.signer.app.CyberCoreSignerApplication</mainClass>
|
||||
<manifestEntries>
|
||||
<Implementation-Title>CyberCore Signer</Implementation-Title>
|
||||
<Implementation-Version>${project.version}</Implementation-Version>
|
||||
<Implementation-Vendor>CyberCore</Implementation-Vendor>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>module-info.class</exclude>
|
||||
<exclude>META-INF/versions/**/module-info.class</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,144 @@
|
||||
package br.com.cybercore.signer.app;
|
||||
|
||||
import br.com.cybercore.signer.core.diagnostics.DiagnosticReport;
|
||||
import br.com.cybercore.signer.core.diagnostics.DiagnosticService;
|
||||
import br.com.cybercore.signer.core.crypto.SigningService;
|
||||
import br.com.cybercore.signer.core.protocol.CcspServer;
|
||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||
import br.com.cybercore.signer.ui.CyberCoreDesktopApplication;
|
||||
import javafx.application.Application;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
/**
|
||||
* Ponto de entrada (Main Class) e bootstrap do agente CyberCore Signer.
|
||||
*/
|
||||
public class CyberCoreSignerApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CyberCoreSignerApplication.class);
|
||||
|
||||
private static boolean developmentMode = false;
|
||||
private static boolean headlessMode = false;
|
||||
private static boolean noWaitMode = false;
|
||||
private static volatile CcspServer runningServer = null;
|
||||
|
||||
public static CcspServer getRunningServer() {
|
||||
return runningServer;
|
||||
}
|
||||
|
||||
public static void stopServer() {
|
||||
if (runningServer != null) {
|
||||
runningServer.stop();
|
||||
runningServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
parseArguments(args);
|
||||
printBanner();
|
||||
|
||||
logger.info("Inicializando CyberCore Signer v0.1.0...");
|
||||
if (developmentMode) {
|
||||
logger.warn("=========================================================");
|
||||
logger.warn(" MODO DE DESENVOLVIMENTO ATIVADO (--dev)");
|
||||
logger.warn(" Logs detalhados ativos. Operação com certificados reais.");
|
||||
logger.warn("=========================================================");
|
||||
}
|
||||
|
||||
DiagnosticService diagnosticService = new DiagnosticService();
|
||||
DiagnosticReport report = diagnosticService.runDiagnostic();
|
||||
logger.info("Ambiente verificado: OS={} {} ({}), Java={} ({})",
|
||||
report.osName(), report.osVersion(), report.osArch(),
|
||||
report.javaVersion(), report.javaVendor());
|
||||
|
||||
if (!report.cryptoEngineOk()) {
|
||||
logger.error("ALERTA CRÍTICO: Engine criptográfica JCA/JCE não está totalmente operacional!");
|
||||
}
|
||||
|
||||
// Se o ambiente tiver suporte gráfico e não for headless forçado, abre a interface JavaFX
|
||||
boolean isHeadless = headlessMode || GraphicsEnvironment.isHeadless();
|
||||
|
||||
if (isHeadless) {
|
||||
logger.info("Executando em modo console / headless (sem interface gráfica).");
|
||||
startHeadlessServer();
|
||||
} else {
|
||||
try {
|
||||
logger.info("Iniciando interface gráfica desktop...");
|
||||
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
|
||||
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());
|
||||
startHeadlessServer();
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("CyberCore Signer finalizado.");
|
||||
}
|
||||
|
||||
private static void startHeadlessServer() {
|
||||
try {
|
||||
CertificateManager certManager = CertificateManager.createDefault();
|
||||
if (developmentMode) {
|
||||
logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema).");
|
||||
}
|
||||
SigningService signingService = new SigningService(certManager);
|
||||
runningServer = new CcspServer(certManager, signingService, null);
|
||||
runningServer.start();
|
||||
logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort());
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
logger.info("Encerrando servidor local CCSP/1...");
|
||||
stopServer();
|
||||
}));
|
||||
|
||||
// Mantém processo vivo para atender requisições web, a menos que em modo no-wait
|
||||
if (!noWaitMode) {
|
||||
Thread.currentThread().join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseArguments(String[] args) {
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) {
|
||||
developmentMode = true;
|
||||
} else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg)
|
||||
|| "--cli".equalsIgnoreCase(arg)) {
|
||||
headlessMode = true;
|
||||
} else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg)
|
||||
|| "--test".equalsIgnoreCase(arg)) {
|
||||
noWaitMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDevelopmentMode() {
|
||||
return developmentMode;
|
||||
}
|
||||
|
||||
public static boolean isHeadlessMode() {
|
||||
return headlessMode;
|
||||
}
|
||||
|
||||
private static void printBanner() {
|
||||
System.out.println("""
|
||||
============================================================
|
||||
____ _ ____
|
||||
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___
|
||||
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\
|
||||
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/
|
||||
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___|
|
||||
|___/
|
||||
SIGNER AGENT v0.1.0
|
||||
============================================================
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
||||
<logger name="br.com.cybercore.signer" level="DEBUG" />
|
||||
</configuration>
|
||||
@@ -0,0 +1,37 @@
|
||||
package br.com.cybercore.signer.app;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
class ArchitectureTest {
|
||||
|
||||
private final JavaClasses importedClasses = new ClassFileImporter()
|
||||
.importPackages("br.com.cybercore.signer");
|
||||
|
||||
@Test
|
||||
@DisplayName("O módulo Core não deve depender de classes dos pacotes UI ou App")
|
||||
void coreShouldNotDependOnUiOrApp() {
|
||||
ArchRule rule = noClasses()
|
||||
.that().resideInAPackage("br.com.cybercore.signer.core..")
|
||||
.should().dependOnClassesThat()
|
||||
.resideInAnyPackage("br.com.cybercore.signer.ui..", "br.com.cybercore.signer.app..");
|
||||
|
||||
rule.check(importedClasses);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("O módulo Core não deve conter dependências de bibliotecas JavaFX")
|
||||
void coreShouldNotDependOnJavaFx() {
|
||||
ArchRule rule = noClasses()
|
||||
.that().resideInAPackage("br.com.cybercore.signer.core..")
|
||||
.should().dependOnClassesThat()
|
||||
.resideInAPackage("javafx..");
|
||||
|
||||
rule.check(importedClasses);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package br.com.cybercore.signer.app;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
class CyberCoreSignerApplicationTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve inicializar a aplicação com argumentos sem erros")
|
||||
void shouldBootstrapWithoutErrors() {
|
||||
try {
|
||||
assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait"}))
|
||||
.doesNotThrowAnyException();
|
||||
} finally {
|
||||
CyberCoreSignerApplication.stopServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>br.com.cybercore.signer</groupId>
|
||||
<artifactId>cybercore-signer-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>cybercore-core</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>CyberCore Signer :: Core</name>
|
||||
<description>Núcleo de domínio, abstrações criptográficas, provedores A1/A3, protocolo CCSP e servidor local.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PDF (PAdES) e Criptografia CMS/PKCS#7 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcutil-jdk18on</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Testes -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,43 @@
|
||||
package br.com.cybercore.signer.core.config;
|
||||
|
||||
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Modelo de configurações gerais e de segurança do CyberCore Signer.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record AgentConfiguration(
|
||||
boolean requireConfirmation,
|
||||
boolean alwaysPromptPin,
|
||||
boolean autoStart,
|
||||
String defaultSealThemeId,
|
||||
CpfDisplayMode defaultCpfDisplayMode,
|
||||
boolean defaultIncludeDateTime,
|
||||
int serverPort
|
||||
) {
|
||||
|
||||
public AgentConfiguration {
|
||||
if (defaultSealThemeId == null || defaultSealThemeId.isBlank()) {
|
||||
defaultSealThemeId = "icp-brasil";
|
||||
}
|
||||
if (defaultCpfDisplayMode == null) {
|
||||
defaultCpfDisplayMode = CpfDisplayMode.FULL;
|
||||
}
|
||||
if (serverPort <= 0) {
|
||||
serverPort = 18420;
|
||||
}
|
||||
}
|
||||
|
||||
public static AgentConfiguration defaultConfiguration() {
|
||||
return new AgentConfiguration(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"icp-brasil",
|
||||
CpfDisplayMode.FULL,
|
||||
true,
|
||||
18420
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package br.com.cybercore.signer.core.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Serviço de gerenciamento e persistência das configurações do CyberCore Signer.
|
||||
* Salva e recupera as preferências do usuário no arquivo ~/.cybercore/config.json.
|
||||
*/
|
||||
public final class AgentConfigurationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AgentConfigurationService.class);
|
||||
private static final String CONFIG_FILE_NAME = "config.json";
|
||||
|
||||
private static final AgentConfigurationService INSTANCE = new AgentConfigurationService();
|
||||
|
||||
private final Path configPath;
|
||||
private final ObjectMapper objectMapper;
|
||||
private AgentConfiguration currentConfig;
|
||||
|
||||
public static AgentConfigurationService getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public AgentConfigurationService() {
|
||||
this(resolveDefaultConfigPath());
|
||||
}
|
||||
|
||||
public AgentConfigurationService(Path configPath) {
|
||||
this.configPath = Objects.requireNonNull(configPath);
|
||||
this.objectMapper = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
this.currentConfig = load();
|
||||
}
|
||||
|
||||
private static Path resolveDefaultConfigPath() {
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
return Path.of(userHome, ".cybercore", CONFIG_FILE_NAME);
|
||||
}
|
||||
|
||||
public synchronized AgentConfiguration getConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
public synchronized AgentConfiguration loadConfiguration() {
|
||||
return getConfig();
|
||||
}
|
||||
|
||||
public synchronized void updateConfig(AgentConfiguration newConfig) {
|
||||
Objects.requireNonNull(newConfig, "Configuração não pode ser nula.");
|
||||
this.currentConfig = newConfig;
|
||||
save();
|
||||
}
|
||||
|
||||
public synchronized void saveConfiguration(AgentConfiguration newConfig) {
|
||||
updateConfig(newConfig);
|
||||
}
|
||||
|
||||
private AgentConfiguration load() {
|
||||
try {
|
||||
if (Files.exists(configPath)) {
|
||||
AgentConfiguration cfg = objectMapper.readValue(configPath.toFile(), AgentConfiguration.class);
|
||||
logger.info("Configurações do agente carregadas de: {}", configPath);
|
||||
return cfg;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler {}. Usando configurações padrão: {}", configPath, e.getMessage());
|
||||
}
|
||||
return AgentConfiguration.defaultConfiguration();
|
||||
}
|
||||
|
||||
private void save() {
|
||||
try {
|
||||
if (configPath.getParent() != null && !Files.exists(configPath.getParent())) {
|
||||
Files.createDirectories(configPath.getParent());
|
||||
}
|
||||
objectMapper.writeValue(configPath.toFile(), currentConfig);
|
||||
logger.info("Configurações do agente salvas com sucesso em: {}", configPath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Erro ao salvar configurações em {}: {}", configPath, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getConfigPath() {
|
||||
return configPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package br.com.cybercore.signer.core.crypto;
|
||||
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||
|
||||
/**
|
||||
* Algoritmos criptográficos de assinatura suportados pelo CyberCore Signer.
|
||||
*/
|
||||
public enum DigestAlgorithm {
|
||||
SHA256_WITH_RSA("SHA256withRSA", "SHA-256", 32),
|
||||
SHA384_WITH_RSA("SHA384withRSA", "SHA-384", 48),
|
||||
SHA512_WITH_RSA("SHA512withRSA", "SHA-512", 64),
|
||||
SHA256_WITH_ECDSA("SHA256withECDSA", "SHA-256", 32),
|
||||
SHA384_WITH_ECDSA("SHA384withECDSA", "SHA-384", 48),
|
||||
SHA512_WITH_ECDSA("SHA512withECDSA", "SHA-512", 64);
|
||||
|
||||
private final String jcaName;
|
||||
private final String digestName;
|
||||
private final int expectedDigestLengthBytes;
|
||||
|
||||
DigestAlgorithm(String jcaName, String digestName, int expectedDigestLengthBytes) {
|
||||
this.jcaName = jcaName;
|
||||
this.digestName = digestName;
|
||||
this.expectedDigestLengthBytes = expectedDigestLengthBytes;
|
||||
}
|
||||
|
||||
public String getJcaName() {
|
||||
return jcaName;
|
||||
}
|
||||
|
||||
public String getDigestName() {
|
||||
return digestName;
|
||||
}
|
||||
|
||||
public int getExpectedDigestLengthBytes() {
|
||||
return expectedDigestLengthBytes;
|
||||
}
|
||||
|
||||
public static DigestAlgorithm fromName(String name) throws CertificateProviderException {
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new CertificateProviderException(CcspErrorCode.UNSUPPORTED_ALGORITHM, "Algoritmo não especificado.");
|
||||
}
|
||||
for (DigestAlgorithm alg : values()) {
|
||||
if (alg.jcaName.equalsIgnoreCase(name.trim())) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.UNSUPPORTED_ALGORITHM,
|
||||
"Algoritmo não suportado pelo CyberCore Signer: " + name
|
||||
);
|
||||
}
|
||||
|
||||
public void validateDigestLength(byte[] digest) throws CertificateProviderException {
|
||||
if (digest == null) {
|
||||
throw new CertificateProviderException(CcspErrorCode.SIGNATURE_FAILED, "Digest não pode ser nulo.");
|
||||
}
|
||||
if (digest.length != expectedDigestLengthBytes) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
String.format("Tamanho inválido para digest %s: esperado %d bytes, recebido %d bytes.",
|
||||
digestName, expectedDigestLengthBytes, digest.length)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package br.com.cybercore.signer.core.crypto;
|
||||
|
||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||
import br.com.cybercore.signer.core.provider.CertificateProvider;
|
||||
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Serviço central de assinatura digital do CyberCore Signer.
|
||||
* Orquestra validações rigorosas de protocolo e algoritmos, resolução de certificados
|
||||
* e delegação aos provedores criptográficos JCA/JCE (Windows-MY, A1, PKCS#11).
|
||||
* Nunca implementa algoritmos criptográficos manuais e nunca expõe a chave privada.
|
||||
*/
|
||||
public class SigningService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SigningService.class);
|
||||
|
||||
private final CertificateManager certificateManager;
|
||||
|
||||
public SigningService(CertificateManager certificateManager) {
|
||||
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa a assinatura digital local de um digest fornecido por uma aplicação autorizada.
|
||||
*
|
||||
* @param context Contexto da requisição contendo digest, algoritmo, ID da requisição e certificado indicado.
|
||||
* @return Resultado imutável da assinatura com os bytes assinados e certificados em Base64.
|
||||
* @throws CertificateProviderException Se qualquer validação, dispositivo ou operação criptográfica falhar.
|
||||
*/
|
||||
public SigningResult sign(SigningContext context) throws CertificateProviderException {
|
||||
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
|
||||
|
||||
if (context.requestId() == null || context.requestId().isBlank()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
"O campo 'request_id' é obrigatório para correlação de assinatura."
|
||||
);
|
||||
}
|
||||
|
||||
// 1. Validação de algoritmo suportado e tamanho do digest
|
||||
DigestAlgorithm algorithm = DigestAlgorithm.fromName(context.algorithm());
|
||||
algorithm.validateDigestLength(context.digest());
|
||||
|
||||
// 2. Resolução do certificado alvo
|
||||
ManagedCertificate targetCertificate = resolveCertificate(context.certificateHint());
|
||||
|
||||
// 3. Validação do ciclo de vida e disponibilidade de chave privada
|
||||
validateCertificateState(targetCertificate);
|
||||
|
||||
// 4. Obtenção do provedor responsável
|
||||
CertificateProvider provider = certificateManager.getProviderForCertificate(targetCertificate.id());
|
||||
if (provider == null || !provider.isAvailable()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
|
||||
"O provedor para o certificado '" + targetCertificate.alias() + "' não está disponível no sistema."
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("Assinando digest para requestId='{}', appId='{}', cert='{}', provider='{}', alg='{}'",
|
||||
context.requestId(), context.applicationId(), targetCertificate.alias(),
|
||||
provider.getProviderId(), algorithm.getJcaName());
|
||||
|
||||
// Se o contexto não tiver hint explícito, utiliza o ID do certificado resolvido
|
||||
SigningContext effectiveContext = (context.certificateHint() == null || context.certificateHint().isBlank())
|
||||
? new SigningContext(context.requestId(), context.applicationId(), context.digest(),
|
||||
context.algorithm(), targetCertificate.id(), context.requestedAt())
|
||||
: context;
|
||||
|
||||
// 5. Execução no provedor JCA/JCE
|
||||
SigningResult result = provider.sign(effectiveContext);
|
||||
|
||||
if (result == null || result.signatureBytes() == null || result.signatureBytes().length == 0) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
"O provedor retornou uma assinatura nula ou vazia."
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("Assinatura digital concluída com sucesso para requestId='{}'. Tamanho assinatura: {} bytes",
|
||||
context.requestId(), result.signatureBytes().length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ManagedCertificate resolveCertificate(String hint) throws CertificateProviderException {
|
||||
if (hint != null && !hint.isBlank()) {
|
||||
Optional<ManagedCertificate> found = certificateManager.findByHint(hint);
|
||||
if (found.isEmpty()) {
|
||||
// Tenta rediscovery caso um token tenha acabado de ser conectado
|
||||
certificateManager.discoverCertificates();
|
||||
found = certificateManager.findByHint(hint);
|
||||
}
|
||||
if (found.isPresent()) {
|
||||
return found.get();
|
||||
}
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Nenhum certificado encontrado para o identificador ou hint: " + hint
|
||||
);
|
||||
}
|
||||
|
||||
// Sem hint: seleciona automaticamente se houver apenas um certificado operacional disponível
|
||||
List<ManagedCertificate> available = certificateManager.getAvailableCertificates();
|
||||
if (available.isEmpty()) {
|
||||
// Tenta redescobrir
|
||||
certificateManager.discoverCertificates();
|
||||
available = certificateManager.getAvailableCertificates();
|
||||
}
|
||||
|
||||
if (available.isEmpty()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Nenhum certificado com chave privada disponível foi encontrado na máquina."
|
||||
);
|
||||
}
|
||||
|
||||
if (available.size() == 1) {
|
||||
return available.get(0);
|
||||
}
|
||||
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Existem múltiplos certificados disponíveis (" + available.size() +
|
||||
"). Especifique o 'certificate_hint' para selecionar o certificado desejado."
|
||||
);
|
||||
}
|
||||
|
||||
private void validateCertificateState(ManagedCertificate cert) throws CertificateProviderException {
|
||||
if (cert.status() == CertificateStatus.EXPIRED) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_EXPIRED,
|
||||
"O certificado '" + cert.alias() + "' está expirado desde " + cert.notAfter()
|
||||
);
|
||||
}
|
||||
if (cert.status() == CertificateStatus.NOT_YET_VALID) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_INVALID,
|
||||
"O certificado '" + cert.alias() + "' ainda não é válido. Início da validade: " + cert.notBefore()
|
||||
);
|
||||
}
|
||||
if (cert.status() == CertificateStatus.NO_PRIVATE_KEY) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
|
||||
"O certificado '" + cert.alias() + "' não possui chave privada acessível para assinatura."
|
||||
);
|
||||
}
|
||||
if (cert.status() == CertificateStatus.REVOKED) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_INVALID,
|
||||
"O certificado '" + cert.alias() + "' consta como revogado."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public CertificateManager getCertificateManager() {
|
||||
return certificateManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package br.com.cybercore.signer.core.crypto;
|
||||
|
||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.CertificateType;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utilitários para parsing, inspeção e extração de metadados de certificados X.509
|
||||
* com suporte nativo a extensões da Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil).
|
||||
*/
|
||||
public final class X509CertificateUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(X509CertificateUtils.class);
|
||||
|
||||
private static final Pattern CN_PATTERN = Pattern.compile("CN=([^,]+)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern CPF_PATTERN = Pattern.compile("(\\d{11})");
|
||||
private static final Pattern CNPJ_PATTERN = Pattern.compile("(\\d{14})");
|
||||
|
||||
private X509CertificateUtils() {
|
||||
// Utilitário estático
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodifica um certificado X.509 a partir de bytes codificados (DER ou PEM).
|
||||
*/
|
||||
public static X509Certificate parseCertificate(byte[] encoded) throws CertificateException {
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(encoded));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte um X509Certificate em um ManagedCertificate imutável de domínio.
|
||||
*/
|
||||
public static ManagedCertificate toManagedCertificate(
|
||||
String id,
|
||||
String alias,
|
||||
X509Certificate cert,
|
||||
CertificateType type,
|
||||
boolean hasPrivateKey,
|
||||
String providerId,
|
||||
String hardwareDeviceName
|
||||
) {
|
||||
String subject = cert.getSubjectX500Principal().getName();
|
||||
String issuer = cert.getIssuerX500Principal().getName();
|
||||
String serial = cert.getSerialNumber().toString(16).toUpperCase();
|
||||
|
||||
Instant notBefore = cert.getNotBefore().toInstant();
|
||||
Instant notAfter = cert.getNotAfter().toInstant();
|
||||
Instant now = Instant.now();
|
||||
|
||||
CertificateStatus status;
|
||||
if (!hasPrivateKey) {
|
||||
status = CertificateStatus.NO_PRIVATE_KEY;
|
||||
} else if (now.isBefore(notBefore)) {
|
||||
status = CertificateStatus.NOT_YET_VALID;
|
||||
} else if (now.isAfter(notAfter)) {
|
||||
status = CertificateStatus.EXPIRED;
|
||||
} else {
|
||||
status = CertificateStatus.AVAILABLE;
|
||||
}
|
||||
|
||||
String cn = extractCommonName(subject);
|
||||
String sha1 = calculateFingerprint(cert, "SHA-1");
|
||||
String sha256 = calculateFingerprint(cert, "SHA-256");
|
||||
|
||||
String cpf = extractCpf(cert, cn);
|
||||
String cnpj = extractCnpj(cert, cn);
|
||||
|
||||
int keySize = extractKeySize(cert);
|
||||
String alg = cert.getPublicKey().getAlgorithm();
|
||||
|
||||
return new ManagedCertificate(
|
||||
id,
|
||||
alias,
|
||||
type,
|
||||
status,
|
||||
subject,
|
||||
issuer,
|
||||
serial,
|
||||
sha1,
|
||||
sha256,
|
||||
notBefore,
|
||||
notAfter,
|
||||
alg,
|
||||
keySize,
|
||||
cn,
|
||||
cpf,
|
||||
cnpj,
|
||||
providerId,
|
||||
hardwareDeviceName
|
||||
);
|
||||
}
|
||||
|
||||
public static String extractCommonName(String dn) {
|
||||
if (dn == null) return "Desconhecido";
|
||||
Matcher matcher = CN_PATTERN.matcher(dn);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1).trim();
|
||||
}
|
||||
return dn;
|
||||
}
|
||||
|
||||
public static String calculateFingerprint(X509Certificate cert, String algorithm) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(algorithm);
|
||||
byte[] digest = md.digest(cert.getEncoded());
|
||||
return HexFormat.ofDelimiter(":").formatHex(digest).toUpperCase();
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
logger.debug("Não foi possível calcular fingerprint {} do certificado: {}", algorithm, e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static int extractKeySize(X509Certificate cert) {
|
||||
try {
|
||||
String alg = cert.getPublicKey().getAlgorithm();
|
||||
if ("RSA".equalsIgnoreCase(alg)) {
|
||||
java.security.interfaces.RSAPublicKey rsa = (java.security.interfaces.RSAPublicKey) cert.getPublicKey();
|
||||
return rsa.getModulus().bitLength();
|
||||
} else if ("EC".equalsIgnoreCase(alg) || "ECDSA".equalsIgnoreCase(alg)) {
|
||||
java.security.interfaces.ECPublicKey ec = (java.security.interfaces.ECPublicKey) cert.getPublicKey();
|
||||
return ec.getParams().getCurve().getField().getFieldSize();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Se falhar em extrair tamanho específico da chave, retorna 0
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenta extrair CPF do certificado (via ICP-Brasil SAN ou padrão comum no CN: "NOME:12345678901").
|
||||
*/
|
||||
public static String extractCpf(X509Certificate cert, String commonName) {
|
||||
// Tenta padrão CN ICP-Brasil comum ("FULANO DE TAL:00011122233")
|
||||
if (commonName != null && commonName.contains(":")) {
|
||||
String[] parts = commonName.split(":");
|
||||
if (parts.length > 1) {
|
||||
String candidate = parts[parts.length - 1].trim();
|
||||
if (CPF_PATTERN.matcher(candidate).matches()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tenta SubjectAlternativeNames
|
||||
try {
|
||||
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
|
||||
if (sans != null) {
|
||||
for (List<?> item : sans) {
|
||||
if (item.size() >= 2 && item.get(1) instanceof byte[] bytes) {
|
||||
String content = new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
Matcher m = CPF_PATTERN.matcher(content);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// SAN não disponível ou inacessível
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenta extrair CNPJ do certificado para pessoas jurídicas.
|
||||
*/
|
||||
public static String extractCnpj(X509Certificate cert, String commonName) {
|
||||
if (commonName != null && commonName.contains(":")) {
|
||||
String[] parts = commonName.split(":");
|
||||
if (parts.length > 1) {
|
||||
String candidate = parts[parts.length - 1].trim();
|
||||
if (CNPJ_PATTERN.matcher(candidate).matches()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
|
||||
if (sans != null) {
|
||||
for (List<?> item : sans) {
|
||||
if (item.size() >= 2 && item.get(1) instanceof byte[] bytes) {
|
||||
String content = new String(bytes, StandardCharsets.ISO_8859_1);
|
||||
Matcher m = CNPJ_PATTERN.matcher(content);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// SAN não disponível ou inacessível
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai o nome amigável do titular (removendo sufixo :CPF ou :CNPJ comum em ICP-Brasil).
|
||||
*/
|
||||
public static String extractHolderName(String commonName) {
|
||||
if (commonName == null || commonName.isBlank()) {
|
||||
return "Titular do Certificado";
|
||||
}
|
||||
if (commonName.contains(":")) {
|
||||
String[] parts = commonName.split(":");
|
||||
if (parts.length > 0 && !parts[0].isBlank()) {
|
||||
return parts[0].trim();
|
||||
}
|
||||
}
|
||||
return commonName.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package br.com.cybercore.signer.core.diagnostics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Relatório consolidado de integridade e diagnósticos do CyberCore Signer.
|
||||
*/
|
||||
public record DiagnosticReport(
|
||||
Instant generatedAt,
|
||||
String osName,
|
||||
String osVersion,
|
||||
String osArch,
|
||||
String javaVersion,
|
||||
String javaVendor,
|
||||
String javaRuntime,
|
||||
boolean isWindows,
|
||||
boolean cryptoEngineOk,
|
||||
List<String> registeredSecurityProviders,
|
||||
boolean windowsStoreSupported,
|
||||
boolean pkcs11ModuleAvailable,
|
||||
int totalCertificatesFound,
|
||||
int a1CertificatesCount,
|
||||
int a3CertificatesCount,
|
||||
boolean localServerRunning,
|
||||
int localServerPort,
|
||||
Map<String, String> additionalDetails
|
||||
) implements Serializable {
|
||||
|
||||
public DiagnosticReport {
|
||||
registeredSecurityProviders = registeredSecurityProviders != null
|
||||
? List.copyOf(registeredSecurityProviders)
|
||||
: Collections.emptyList();
|
||||
additionalDetails = additionalDetails != null
|
||||
? Map.copyOf(additionalDetails)
|
||||
: Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> registeredSecurityProviders() {
|
||||
return Collections.unmodifiableList(registeredSecurityProviders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> additionalDetails() {
|
||||
return Collections.unmodifiableMap(additionalDetails);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package br.com.cybercore.signer.core.diagnostics;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Serviço responsável por verificar as capacidades do sistema, JVM e engines criptográficas.
|
||||
*/
|
||||
public class DiagnosticService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DiagnosticService.class);
|
||||
|
||||
public DiagnosticReport runDiagnostic() {
|
||||
logger.info("Iniciando diagnóstico do ambiente e engine criptográfica...");
|
||||
|
||||
String osName = System.getProperty("os.name", "Desconhecido");
|
||||
String osVersion = System.getProperty("os.version", "Desconhecido");
|
||||
String osArch = System.getProperty("os.arch", "Desconhecido");
|
||||
boolean isWindows = osName.toLowerCase().contains("win");
|
||||
|
||||
String javaVersion = System.getProperty("java.version", "Desconhecido");
|
||||
String javaVendor = System.getProperty("java.vendor", "Desconhecido");
|
||||
String javaRuntime = System.getProperty("java.runtime.name", "Desconhecido");
|
||||
|
||||
List<String> providersList = new ArrayList<>();
|
||||
boolean windowsStoreSupported = false;
|
||||
boolean pkcs11ModuleAvailable = false;
|
||||
|
||||
for (Provider p : Security.getProviders()) {
|
||||
providersList.add(p.getName() + " v" + p.getVersionStr());
|
||||
if ("SunMSCAPI".equalsIgnoreCase(p.getName())) {
|
||||
windowsStoreSupported = true;
|
||||
}
|
||||
if ("SunPKCS11".equalsIgnoreCase(p.getName())) {
|
||||
pkcs11ModuleAvailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean cryptoEngineOk = verifyCryptoAlgorithms();
|
||||
|
||||
Map<String, String> details = new HashMap<>();
|
||||
details.put("VirtualThreadsSupported", "true");
|
||||
details.put("DefaultEncoding", System.getProperty("file.encoding", "UTF-8"));
|
||||
|
||||
return new DiagnosticReport(
|
||||
Instant.now(),
|
||||
osName,
|
||||
osVersion,
|
||||
osArch,
|
||||
javaVersion,
|
||||
javaVendor,
|
||||
javaRuntime,
|
||||
isWindows,
|
||||
cryptoEngineOk,
|
||||
providersList,
|
||||
windowsStoreSupported,
|
||||
pkcs11ModuleAvailable,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
39741,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
private boolean verifyCryptoAlgorithms() {
|
||||
try {
|
||||
Signature.getInstance("SHA256withRSA");
|
||||
Signature.getInstance("SHA384withRSA");
|
||||
Signature.getInstance("SHA512withRSA");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("Falha ao inicializar algoritmos padrão JCA/JCE: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package br.com.cybercore.signer.core.domain;
|
||||
|
||||
/**
|
||||
* Estado de validade e disponibilidade de um certificado.
|
||||
*/
|
||||
public enum CertificateStatus {
|
||||
AVAILABLE("Disponível", true),
|
||||
EXPIRED("Expirado", false),
|
||||
NOT_YET_VALID("Ainda não válido", false),
|
||||
NO_PRIVATE_KEY("Sem chave privada", false),
|
||||
TOKEN_DISCONNECTED("Token desconectado", false),
|
||||
REVOKED("Revogado", false),
|
||||
UNKNOWN("Desconhecido", false);
|
||||
|
||||
private final String description;
|
||||
private final boolean usableForSigning;
|
||||
|
||||
CertificateStatus(String description, boolean usableForSigning) {
|
||||
this.description = description;
|
||||
this.usableForSigning = usableForSigning;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isUsableForSigning() {
|
||||
return usableForSigning;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package br.com.cybercore.signer.core.domain;
|
||||
|
||||
/**
|
||||
* Tipo de certificado digital suportado pelo CyberCore Signer.
|
||||
*/
|
||||
public enum CertificateType {
|
||||
/**
|
||||
* Certificado em software (.pfx / .p12).
|
||||
*/
|
||||
A1("A1", "Certificado Local"),
|
||||
|
||||
/**
|
||||
* Certificado em hardware (Smart Card / Token USB / PKCS#11 / Windows Store).
|
||||
*/
|
||||
A3("A3", "Token / Smart Card");
|
||||
|
||||
private final String code;
|
||||
private final String label;
|
||||
|
||||
CertificateType(String code, String label) {
|
||||
this.code = code;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package br.com.cybercore.signer.core.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Representação imutável de um certificado digital gerenciado pelo CyberCore Signer.
|
||||
* Contém apenas metadados públicos e nunca expõe referências diretas de chave privada.
|
||||
*/
|
||||
public record ManagedCertificate(
|
||||
String id,
|
||||
String alias,
|
||||
CertificateType type,
|
||||
CertificateStatus status,
|
||||
String subjectDn,
|
||||
String issuerDn,
|
||||
String serialNumber,
|
||||
String sha1Fingerprint,
|
||||
String sha256Fingerprint,
|
||||
Instant notBefore,
|
||||
Instant notAfter,
|
||||
String algorithm,
|
||||
int keySize,
|
||||
String commonName,
|
||||
String cpf,
|
||||
String cnpj,
|
||||
String providerId,
|
||||
String hardwareDeviceName
|
||||
) implements Serializable {
|
||||
|
||||
public ManagedCertificate {
|
||||
Objects.requireNonNull(id, "id must not be null");
|
||||
Objects.requireNonNull(alias, "alias must not be null");
|
||||
Objects.requireNonNull(type, "type must not be null");
|
||||
Objects.requireNonNull(status, "status must not be null");
|
||||
Objects.requireNonNull(subjectDn, "subjectDn must not be null");
|
||||
Objects.requireNonNull(issuerDn, "issuerDn must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o certificado está atualmente dentro do período de validade temporal.
|
||||
*/
|
||||
public boolean isCurrentlyValid() {
|
||||
Instant now = Instant.now();
|
||||
return (notBefore == null || !now.isBefore(notBefore))
|
||||
&& (notAfter == null || !now.isAfter(notAfter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o certificado está apto a assinar.
|
||||
*/
|
||||
public boolean canSign() {
|
||||
return status.isUsableForSigning() && isCurrentlyValid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package br.com.cybercore.signer.core.domain;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Contexto da operação de assinatura a ser processada pelo SigningService / CertificateProvider.
|
||||
*/
|
||||
public record SigningContext(
|
||||
String requestId,
|
||||
String applicationId,
|
||||
byte[] digest,
|
||||
String algorithm,
|
||||
String certificateHint,
|
||||
Instant requestedAt
|
||||
) {
|
||||
public SigningContext {
|
||||
Objects.requireNonNull(requestId, "requestId must not be null");
|
||||
Objects.requireNonNull(applicationId, "applicationId must not be null");
|
||||
Objects.requireNonNull(digest, "digest must not be null");
|
||||
Objects.requireNonNull(algorithm, "algorithm must not be null");
|
||||
digest = digest.clone();
|
||||
if (requestedAt == null) {
|
||||
requestedAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] digest() {
|
||||
return digest.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SigningContext that = (SigningContext) o;
|
||||
return Objects.equals(requestId, that.requestId)
|
||||
&& Objects.equals(applicationId, that.applicationId)
|
||||
&& Arrays.equals(digest, that.digest)
|
||||
&& Objects.equals(algorithm, that.algorithm)
|
||||
&& Objects.equals(certificateHint, that.certificateHint)
|
||||
&& Objects.equals(requestedAt, that.requestedAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(requestId, applicationId, algorithm, certificateHint, requestedAt);
|
||||
result = 31 * result + Arrays.hashCode(digest);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package br.com.cybercore.signer.core.domain;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Contêiner seguro para material criptográfico de assinatura (chave privada e cadeia X.509).
|
||||
* Permite que serviços de alto nível (PDF/PAdES e XMLDSig) realizem assinaturas estruturadas.
|
||||
* A chave privada e a cadeia permanecem estritamente na máquina local do usuário.
|
||||
*/
|
||||
public record SigningKeyMaterial(
|
||||
PrivateKey privateKey,
|
||||
X509Certificate[] certificateChain,
|
||||
Provider jcaProvider
|
||||
) {
|
||||
|
||||
public SigningKeyMaterial {
|
||||
Objects.requireNonNull(privateKey, "Chave privada não pode ser nula.");
|
||||
Objects.requireNonNull(certificateChain, "Cadeia de certificados não pode ser nula.");
|
||||
if (certificateChain.length == 0) {
|
||||
throw new IllegalArgumentException("A cadeia de certificados deve conter ao menos um certificado.");
|
||||
}
|
||||
// Cópia defensiva
|
||||
certificateChain = Arrays.copyOf(certificateChain, certificateChain.length);
|
||||
}
|
||||
|
||||
public X509Certificate signingCertificate() {
|
||||
return certificateChain[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] certificateChain() {
|
||||
return Arrays.copyOf(certificateChain, certificateChain.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package br.com.cybercore.signer.core.domain;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Resultado da operação de assinatura digital.
|
||||
*/
|
||||
public record SigningResult(
|
||||
String requestId,
|
||||
byte[] signatureBytes,
|
||||
String certificateBase64,
|
||||
List<String> certificateChainBase64,
|
||||
String algorithm,
|
||||
Instant signedAt
|
||||
) {
|
||||
public SigningResult {
|
||||
Objects.requireNonNull(requestId, "requestId must not be null");
|
||||
Objects.requireNonNull(signatureBytes, "signatureBytes must not be null");
|
||||
Objects.requireNonNull(certificateBase64, "certificateBase64 must not be null");
|
||||
Objects.requireNonNull(algorithm, "algorithm must not be null");
|
||||
signatureBytes = signatureBytes.clone();
|
||||
certificateChainBase64 = certificateChainBase64 != null
|
||||
? List.copyOf(certificateChainBase64)
|
||||
: Collections.singletonList(certificateBase64);
|
||||
if (signedAt == null) {
|
||||
signedAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] signatureBytes() {
|
||||
return signatureBytes.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> certificateChainBase64() {
|
||||
return Collections.unmodifiableList(certificateChainBase64);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SigningResult that = (SigningResult) o;
|
||||
return Objects.equals(requestId, that.requestId)
|
||||
&& Arrays.equals(signatureBytes, that.signatureBytes)
|
||||
&& Objects.equals(certificateBase64, that.certificateBase64)
|
||||
&& Objects.equals(certificateChainBase64, that.certificateChainBase64)
|
||||
&& Objects.equals(algorithm, that.algorithm)
|
||||
&& Objects.equals(signedAt, that.signedAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(requestId, certificateBase64, certificateChainBase64, algorithm, signedAt);
|
||||
result = 31 * result + Arrays.hashCode(signatureBytes);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package br.com.cybercore.signer.core.pdf;
|
||||
|
||||
/**
|
||||
* Seleção da página do PDF na qual o selo visual da assinatura digital será carimbado.
|
||||
*/
|
||||
public enum PdfSealPageChoice {
|
||||
LAST_PAGE("Última Página (Recomendado)"),
|
||||
FIRST_PAGE("Primeira Página"),
|
||||
ALL_PAGES("Todas as Páginas");
|
||||
|
||||
private final String description;
|
||||
|
||||
PdfSealPageChoice(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package br.com.cybercore.signer.core.pdf;
|
||||
|
||||
/**
|
||||
* Posição do selo visual de assinatura na página do documento PDF.
|
||||
*/
|
||||
public enum PdfSealPosition {
|
||||
BOTTOM_RIGHT("Canto Inferior Direito"),
|
||||
BOTTOM_LEFT("Canto Inferior Esquerdo"),
|
||||
TOP_RIGHT("Canto Superior Direito"),
|
||||
TOP_LEFT("Canto Superior Esquerdo"),
|
||||
CUSTOM("Posição Personalizada por Clique na Página");
|
||||
|
||||
private final String description;
|
||||
|
||||
PdfSealPosition(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package br.com.cybercore.signer.core.pdf;
|
||||
|
||||
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 java.util.Objects;
|
||||
|
||||
/**
|
||||
* Parâmetros de configuração para a operação de assinatura digital PAdES com selo visual em PDF,
|
||||
* incluindo suporte a posicionamento predefinido ou coordenadas personalizadas na página,
|
||||
* e preferências granulares para exibição de CPF e data/hora.
|
||||
*/
|
||||
public record 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
|
||||
) {
|
||||
|
||||
public PdfSignatureOptions {
|
||||
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
|
||||
if (sealTheme == null) {
|
||||
sealTheme = PdfSealThemeRegistry.getDefaultTheme();
|
||||
}
|
||||
if (position == null) {
|
||||
position = (customX != null && customY != null) ? PdfSealPosition.CUSTOM : PdfSealPosition.BOTTOM_RIGHT;
|
||||
}
|
||||
if (pageChoice == null) {
|
||||
pageChoice = PdfSealPageChoice.LAST_PAGE;
|
||||
}
|
||||
if (reason == null || reason.isBlank()) {
|
||||
reason = "Assinatura Digital de Documento";
|
||||
}
|
||||
if (location == null || location.isBlank()) {
|
||||
location = "Brasil";
|
||||
}
|
||||
if (cpfDisplayMode == null) {
|
||||
cpfDisplayMode = (sealTheme.defaultCpfMode() != null) ? sealTheme.defaultCpfMode() : CpfDisplayMode.FULL;
|
||||
}
|
||||
if (includeDateTime == null) {
|
||||
includeDateTime = sealTheme.defaultIncludeDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor para compatibilidade com assinaturas anteriores a customExtraInfo.
|
||||
*/
|
||||
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) {
|
||||
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
|
||||
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor de compatibilidade para assinatura com posições pré-fixadas por enum.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
|
||||
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
|
||||
String reason, String location, Float customX, Float customY, Integer targetPageNumber) {
|
||||
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, customX, customY, targetPageNumber, null, null, null);
|
||||
}
|
||||
|
||||
public static Builder builder(String certificateId) {
|
||||
return new Builder(certificateId);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final String certificateId;
|
||||
private char[] pinOrPassword;
|
||||
private boolean visibleSeal = true;
|
||||
private PdfSealTheme sealTheme;
|
||||
private PdfSealPosition position = PdfSealPosition.BOTTOM_RIGHT;
|
||||
private PdfSealPageChoice pageChoice = PdfSealPageChoice.LAST_PAGE;
|
||||
private String reason = "Assinatura Digital ICP-Brasil";
|
||||
private String location = "Brasil";
|
||||
private Float customX;
|
||||
private Float customY;
|
||||
private Integer targetPageNumber;
|
||||
private CpfDisplayMode cpfDisplayMode;
|
||||
private Boolean includeDateTime;
|
||||
|
||||
public Builder(String certificateId) {
|
||||
this.certificateId = Objects.requireNonNull(certificateId);
|
||||
}
|
||||
|
||||
public Builder pinOrPassword(char[] pin) {
|
||||
this.pinOrPassword = pin;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder visibleSeal(boolean visible) {
|
||||
this.visibleSeal = visible;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sealTheme(PdfSealTheme theme) {
|
||||
this.sealTheme = theme;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder position(PdfSealPosition pos) {
|
||||
this.position = pos;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder pageChoice(PdfSealPageChoice choice) {
|
||||
this.pageChoice = choice;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder reason(String reason) {
|
||||
this.reason = reason;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder location(String location) {
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder cpfDisplayMode(CpfDisplayMode mode) {
|
||||
this.cpfDisplayMode = mode;
|
||||
return this;
|
||||
}
|
||||
|
||||
private String customExtraInfo;
|
||||
|
||||
public Builder includeDateTime(boolean include) {
|
||||
this.includeDateTime = include;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder customExtraInfo(String info) {
|
||||
this.customExtraInfo = info;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder customPosition(Float x, Float y, Integer pageNumber) {
|
||||
this.customX = x;
|
||||
this.customY = y;
|
||||
this.targetPageNumber = pageNumber;
|
||||
if (x != null && y != null) {
|
||||
this.position = PdfSealPosition.CUSTOM;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public PdfSignatureOptions build() {
|
||||
return new PdfSignatureOptions(
|
||||
certificateId,
|
||||
pinOrPassword,
|
||||
visibleSeal,
|
||||
sealTheme,
|
||||
position,
|
||||
pageChoice,
|
||||
reason,
|
||||
location,
|
||||
customX,
|
||||
customY,
|
||||
targetPageNumber,
|
||||
cpfDisplayMode,
|
||||
includeDateTime,
|
||||
customExtraInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package br.com.cybercore.signer.core.pdf;
|
||||
|
||||
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;
|
||||
import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer;
|
||||
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||
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.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
import org.bouncycastle.cms.CMSSignedData;
|
||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||
import org.bouncycastle.cms.CMSTypedData;
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Serviço de alto nível para Assinatura Digital de Documentos PDF no padrão PAdES (PDF Advanced Electronic Signatures),
|
||||
* com suporte à aplicação de Selos Visuais estilizados por Temas SVG, posicionamento livre por clique na página e renderização de previews.
|
||||
*/
|
||||
public class PdfSigningService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PdfSigningService.class);
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
|
||||
private final CertificateManager certificateManager;
|
||||
|
||||
public PdfSigningService(CertificateManager certificateManager) {
|
||||
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a quantidade total de páginas contidas no documento PDF.
|
||||
*/
|
||||
public static int getPageCount(byte[] pdfBytes) throws IOException {
|
||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||
return doc.getNumberOfPages();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada.
|
||||
*/
|
||||
public static BufferedImage renderPageImage(byte[] pdfBytes, int pageIndex0Based, float dpi) 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());
|
||||
}
|
||||
PDFRenderer renderer = new PDFRenderer(doc);
|
||||
return renderer.renderImageWithDPI(pageIndex0Based, dpi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza uma página específica do PDF diretamente para bytes de imagem PNG.
|
||||
*/
|
||||
public static byte[] renderPagePngBytes(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException {
|
||||
BufferedImage image = renderPageImage(pdfBytes, pageIndex0Based, dpi);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assina digitalmente um arquivo PDF do sistema de arquivos e grava o resultado no arquivo de destino.
|
||||
*/
|
||||
public void signPdfFile(File sourcePdf, File targetPdf, PdfSignatureOptions options) throws Exception {
|
||||
Objects.requireNonNull(sourcePdf, "Arquivo PDF de origem não pode ser nulo.");
|
||||
Objects.requireNonNull(targetPdf, "Arquivo PDF de destino não pode ser nulo.");
|
||||
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
|
||||
|
||||
byte[] inputBytes = Files.readAllBytes(sourcePdf.toPath());
|
||||
byte[] signedBytes = signPdf(inputBytes, options);
|
||||
|
||||
if (targetPdf.getParentFile() != null) {
|
||||
targetPdf.getParentFile().mkdirs();
|
||||
}
|
||||
try (FileOutputStream fos = new FileOutputStream(targetPdf)) {
|
||||
fos.write(signedBytes);
|
||||
}
|
||||
logger.info("PDF assinado com sucesso e salvo em: {}", targetPdf.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assina digitalmente os bytes de um documento PDF, aplicando o selo visual configurado
|
||||
* e retornando o PDF assinado completo com envelope PAdES.
|
||||
*/
|
||||
public byte[] signPdf(byte[] pdfBytes, PdfSignatureOptions options) throws Exception {
|
||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
|
||||
|
||||
// 1. Localiza o certificado e o material criptográfico (chave e cadeia)
|
||||
Optional<ManagedCertificate> certOpt = certificateManager.findById(options.certificateId());
|
||||
if (certOpt.isEmpty()) {
|
||||
certOpt = certificateManager.findByHint(options.certificateId());
|
||||
}
|
||||
if (certOpt.isEmpty()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Certificado não encontrado no gerenciador para ID: " + options.certificateId()
|
||||
);
|
||||
}
|
||||
|
||||
ManagedCertificate cert = certOpt.get();
|
||||
Optional<SigningKeyMaterial> keyMaterialOpt = certificateManager.getKeyMaterial(cert.id(), options.pinOrPassword());
|
||||
if (keyMaterialOpt.isEmpty()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
|
||||
"Chave privada inacessível para o certificado '" + cert.alias() + "'."
|
||||
);
|
||||
}
|
||||
SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
|
||||
|
||||
// 2. Prepara dados do signatário e resumo para o selo visual
|
||||
String signerDoc = cert.cpf() != null ? "CPF: " + cert.cpf() : (cert.cnpj() != null ? "CNPJ: " + cert.cnpj() : "");
|
||||
String issuerCn = extractIssuerCn(cert.issuerDn());
|
||||
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now());
|
||||
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] pdfDigest = sha256.digest(pdfBytes);
|
||||
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
|
||||
|
||||
PdfSealTheme theme = options.sealTheme();
|
||||
String effectiveExtraInfo = (options.customExtraInfo() != null && !options.customExtraInfo().isBlank())
|
||||
? options.customExtraInfo().trim()
|
||||
: theme.formatCustomField();
|
||||
|
||||
// 3. Monta dados do selo visual
|
||||
PdfSealData sealData = new PdfSealData(
|
||||
cert.commonName() != null ? cert.commonName() : cert.alias(),
|
||||
signerDoc,
|
||||
signingTimeStr,
|
||||
issuerCn,
|
||||
hashShort,
|
||||
options.reason(),
|
||||
options.location(),
|
||||
options.cpfDisplayMode(),
|
||||
options.includeDateTime(),
|
||||
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
|
||||
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
||||
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
|
||||
PDSignature signature = new PDSignature();
|
||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||
signature.setName(cert.commonName());
|
||||
signature.setLocation(options.location());
|
||||
signature.setReason(options.reason());
|
||||
signature.setSignDate(Calendar.getInstance());
|
||||
|
||||
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
|
||||
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)) {
|
||||
int totalPages = doc.getNumberOfPages();
|
||||
if (totalPages == 0) {
|
||||
return pdfBytes;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (int i = startPage; i <= endPage; i++) {
|
||||
PDPage page = doc.getPage(i);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
|
||||
float x;
|
||||
float y;
|
||||
|
||||
switch (options.position()) {
|
||||
case BOTTOM_LEFT -> {
|
||||
x = margin;
|
||||
y = margin;
|
||||
}
|
||||
case TOP_RIGHT -> {
|
||||
x = pageW - sealWidth - margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
case TOP_LEFT -> {
|
||||
x = margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
default -> { // BOTTOM_RIGHT
|
||||
x = pageW - sealWidth - margin;
|
||||
y = margin;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String extractIssuerCn(String dn) {
|
||||
if (dn == null) return "ICP-Brasil";
|
||||
int idx = dn.indexOf("CN=");
|
||||
if (idx >= 0) {
|
||||
int end = dn.indexOf(',', idx);
|
||||
return end > idx ? dn.substring(idx + 3, end).trim() : dn.substring(idx + 3).trim();
|
||||
}
|
||||
return dn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assinador PAdES CMS/PKCS#7 desacoplado via Bouncy Castle.
|
||||
*/
|
||||
private static class PadesBouncyCastleSigner implements SignatureInterface {
|
||||
private final SigningKeyMaterial keyMaterial;
|
||||
|
||||
public PadesBouncyCastleSigner(SigningKeyMaterial keyMaterial) {
|
||||
this.keyMaterial = Objects.requireNonNull(keyMaterial);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] sign(InputStream content) throws IOException {
|
||||
try {
|
||||
byte[] messageBytes = content.readAllBytes();
|
||||
|
||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.build(keyMaterial.privateKey());
|
||||
|
||||
X509Certificate userCert = keyMaterial.certificateChain()[0];
|
||||
|
||||
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
|
||||
new JcaDigestCalculatorProviderBuilder().build())
|
||||
.build(contentSigner, userCert));
|
||||
|
||||
gen.addCertificates(new JcaCertStore(Arrays.asList(keyMaterial.certificateChain())));
|
||||
|
||||
CMSTypedData msg = new CMSProcessableByteArray(messageBytes);
|
||||
CMSSignedData signedData = gen.generate(msg, false);
|
||||
|
||||
return signedData.getEncoded();
|
||||
} catch (Exception e) {
|
||||
logger.error("Falha ao gerar assinatura CMS/PKCS#7 para PDF", e);
|
||||
throw new IOException("Falha criptográfica no Bouncy Castle: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package br.com.cybercore.signer.core.pdf.seal;
|
||||
|
||||
/**
|
||||
* Modo de exibição do CPF do signatário no selo visual de assinatura digital.
|
||||
* Permite balancear autenticidade documental e privacidade de dados (LGPD).
|
||||
*/
|
||||
public enum CpfDisplayMode {
|
||||
|
||||
/**
|
||||
* O CPF não é exibido no selo visual.
|
||||
*/
|
||||
NONE("Ocultar CPF"),
|
||||
|
||||
/**
|
||||
* O CPF é exibido parcialmente mascarado (ex: ***.456.789-**).
|
||||
*/
|
||||
MASKED("Mascarado (***.456.789-**)"),
|
||||
|
||||
/**
|
||||
* O CPF é exibido integralmente (ex: 123.456.789-00).
|
||||
*/
|
||||
FULL("Completo (123.456.789-00)");
|
||||
|
||||
private final String description;
|
||||
|
||||
CpfDisplayMode(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata um CPF bruto ou previamente formatado de acordo com este modo.
|
||||
*
|
||||
* @param rawCpf CPF a ser formatado (com ou sem pontuação)
|
||||
* @return String formatada conforme o modo escolhido, ou vazio se não houver CPF
|
||||
*/
|
||||
public String formatCpf(String rawCpf) {
|
||||
if (this == NONE || rawCpf == null || rawCpf.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String digits = rawCpf.replaceAll("\\D", "");
|
||||
if (digits.length() != 11) {
|
||||
// Se não for um CPF de 11 dígitos, retorna mascarado ou o próprio texto
|
||||
return this == MASKED ? "***.***.***-**" : rawCpf;
|
||||
}
|
||||
|
||||
if (this == MASKED) {
|
||||
// Mascara os 3 primeiros e os 2 dígitos verificadores: ***.456.789-**
|
||||
String part2 = digits.substring(3, 6);
|
||||
String part3 = digits.substring(6, 9);
|
||||
return "***." + part2 + "." + part3 + "-**";
|
||||
}
|
||||
|
||||
// FULL: 123.456.789-00
|
||||
return digits.substring(0, 3) + "." +
|
||||
digits.substring(3, 6) + "." +
|
||||
digits.substring(6, 9) + "-" +
|
||||
digits.substring(9, 11);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package br.com.cybercore.signer.core.pdf.seal;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Gerenciador de armazenamento em disco para Selos Visuais Personalizados criados ou instalados pelo usuário.
|
||||
* Persiste cada selo como um arquivo JSON no diretório ~/.cybercore/seals/.
|
||||
*/
|
||||
public final class CustomSealStorage {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomSealStorage.class);
|
||||
private static final String SEALS_DIR_NAME = "seals";
|
||||
|
||||
private final Path storageDir;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public CustomSealStorage() {
|
||||
this(resolveDefaultStorageDir());
|
||||
}
|
||||
|
||||
public CustomSealStorage(Path storageDir) {
|
||||
this.storageDir = Objects.requireNonNull(storageDir);
|
||||
this.objectMapper = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
ensureDirectoryExists();
|
||||
}
|
||||
|
||||
private static Path resolveDefaultStorageDir() {
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
return Path.of(userHome, ".cybercore", SEALS_DIR_NAME);
|
||||
}
|
||||
|
||||
private void ensureDirectoryExists() {
|
||||
try {
|
||||
if (!Files.exists(storageDir)) {
|
||||
Files.createDirectories(storageDir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Falha ao criar diretório de selos personalizados: {}", storageDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getStorageDir() {
|
||||
return storageDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva ou atualiza um selo personalizado em disco.
|
||||
*/
|
||||
public void saveSeal(PdfSealTheme seal) throws IOException {
|
||||
Objects.requireNonNull(seal, "Selo não pode ser nulo.");
|
||||
ensureDirectoryExists();
|
||||
|
||||
String safeFileName = sanitizeFileName(seal.id()) + ".json";
|
||||
Path targetPath = storageDir.resolve(safeFileName);
|
||||
|
||||
// Marca como personalizado
|
||||
PdfSealTheme customSeal = new PdfSealTheme(
|
||||
seal.id(),
|
||||
seal.name(),
|
||||
seal.description(),
|
||||
seal.width(),
|
||||
seal.height(),
|
||||
seal.primaryColor(),
|
||||
seal.secondaryColor(),
|
||||
seal.backgroundColor(),
|
||||
seal.borderColor(),
|
||||
seal.textColor(),
|
||||
seal.svgTemplate(),
|
||||
seal.customHeader(),
|
||||
seal.customFooter(),
|
||||
seal.iconStyle(),
|
||||
seal.defaultCpfMode(),
|
||||
seal.defaultIncludeDateTime(),
|
||||
true,
|
||||
seal.customLogoBase64(),
|
||||
seal.customFieldLabel(),
|
||||
seal.customFieldValue()
|
||||
);
|
||||
|
||||
objectMapper.writeValue(targetPath.toFile(), customSeal);
|
||||
logger.info("Selo personalizado '{}' instalado com sucesso em: {}", seal.id(), targetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega todos os selos personalizados instalados no diretório do usuário.
|
||||
*/
|
||||
public List<PdfSealTheme> loadAllCustomSeals() {
|
||||
List<PdfSealTheme> seals = new ArrayList<>();
|
||||
if (!Files.exists(storageDir)) {
|
||||
return seals;
|
||||
}
|
||||
|
||||
try (Stream<Path> stream = Files.list(storageDir)) {
|
||||
stream.filter(p -> p.toString().toLowerCase().endsWith(".json"))
|
||||
.forEach(file -> {
|
||||
try {
|
||||
PdfSealTheme seal = objectMapper.readValue(file.toFile(), PdfSealTheme.class);
|
||||
seals.add(seal);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao carregar arquivo de selo {}: {}", file.getFileName(), e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.error("Erro ao listar diretório de selos {}", storageDir, e);
|
||||
}
|
||||
|
||||
return seals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um selo personalizado do disco.
|
||||
*/
|
||||
public boolean deleteSeal(String sealId) {
|
||||
if (sealId == null || sealId.isBlank()) return false;
|
||||
String safeFileName = sanitizeFileName(sealId) + ".json";
|
||||
Path targetPath = storageDir.resolve(safeFileName);
|
||||
|
||||
try {
|
||||
boolean deleted = Files.deleteIfExists(targetPath);
|
||||
if (deleted) {
|
||||
logger.info("Selo personalizado '{}' removido do disco.", sealId);
|
||||
}
|
||||
return deleted;
|
||||
} catch (IOException e) {
|
||||
logger.error("Erro ao excluir selo {}: {}", sealId, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importa um selo a partir de um arquivo externo (.json) e o instala no diretório do usuário.
|
||||
*/
|
||||
public PdfSealTheme importSeal(Path externalFile) throws IOException {
|
||||
Objects.requireNonNull(externalFile, "Arquivo de importação não pode ser nulo.");
|
||||
if (!Files.exists(externalFile)) {
|
||||
throw new IllegalArgumentException("O arquivo de selo especificado não existe: " + externalFile);
|
||||
}
|
||||
|
||||
PdfSealTheme seal = objectMapper.readValue(externalFile.toFile(), PdfSealTheme.class);
|
||||
saveSeal(seal);
|
||||
return seal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporta um selo para um arquivo externo de destino.
|
||||
*/
|
||||
public void exportSeal(PdfSealTheme seal, Path destinationFile) throws IOException {
|
||||
Objects.requireNonNull(seal, "Selo não pode ser nulo.");
|
||||
Objects.requireNonNull(destinationFile, "Destino não pode ser nulo.");
|
||||
|
||||
if (destinationFile.getParent() != null && !Files.exists(destinationFile.getParent())) {
|
||||
Files.createDirectories(destinationFile.getParent());
|
||||
}
|
||||
|
||||
objectMapper.writeValue(destinationFile.toFile(), seal);
|
||||
logger.info("Selo '{}' exportado com sucesso para: {}", seal.id(), destinationFile);
|
||||
}
|
||||
|
||||
private static String sanitizeFileName(String id) {
|
||||
return id.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package br.com.cybercore.signer.core.pdf.seal;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Metadados e dados do signatário a serem gravados e representados no selo visual da assinatura PDF.
|
||||
*/
|
||||
public record PdfSealData(
|
||||
String signerName,
|
||||
String identifier,
|
||||
String signingDate,
|
||||
String issuer,
|
||||
String hashSummary,
|
||||
String reason,
|
||||
String location,
|
||||
CpfDisplayMode cpfDisplayMode,
|
||||
boolean includeDateTime,
|
||||
String customExtraInfo
|
||||
) {
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss (z)")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
|
||||
public PdfSealData {
|
||||
signerName = (signerName != null && !signerName.isBlank()) ? signerName : "Signatário ICP-Brasil";
|
||||
identifier = (identifier != null && !identifier.isBlank()) ? identifier : "";
|
||||
signingDate = (signingDate != null && !signingDate.isBlank()) ? signingDate : FORMATTER.format(Instant.now());
|
||||
issuer = (issuer != null && !issuer.isBlank()) ? issuer : "Autoridade Certificadora ICP-Brasil";
|
||||
hashSummary = (hashSummary != null && !hashSummary.isBlank()) ? hashSummary : "";
|
||||
reason = (reason != null && !reason.isBlank()) ? reason : "Assinatura Digital de Documento";
|
||||
location = (location != null && !location.isBlank()) ? location : "Brasil";
|
||||
if (cpfDisplayMode == null) {
|
||||
cpfDisplayMode = CpfDisplayMode.FULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor para compatibilidade com assinaturas anteriores a customExtraInfo.
|
||||
*/
|
||||
public PdfSealData(String signerName, String identifier, String signingDate, String issuer,
|
||||
String hashSummary, String reason, String location,
|
||||
CpfDisplayMode cpfDisplayMode, boolean includeDateTime) {
|
||||
this(signerName, identifier, signingDate, issuer, hashSummary, reason, location, cpfDisplayMode, includeDateTime, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor de compatibilidade para código que não especifica cpfDisplayMode e includeDateTime.
|
||||
*/
|
||||
public PdfSealData(String signerName, String identifier, String signingDate, String issuer,
|
||||
String hashSummary, String reason, String location) {
|
||||
this(signerName, identifier, signingDate, issuer, hashSummary, reason, location, CpfDisplayMode.FULL, true, null);
|
||||
}
|
||||
|
||||
public static PdfSealData of(String signerName, String identifier, String issuer, String hashSummary) {
|
||||
return new PdfSealData(signerName, identifier, null, issuer, hashSummary, null, null, CpfDisplayMode.FULL, true);
|
||||
}
|
||||
|
||||
public static PdfSealData of(String signerName, String identifier, String issuer, String hashSummary,
|
||||
CpfDisplayMode cpfDisplayMode, boolean includeDateTime) {
|
||||
return new PdfSealData(signerName, identifier, null, issuer, hashSummary, null, null, cpfDisplayMode, includeDateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
package br.com.cybercore.signer.core.pdf.seal;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Renderizador de alta fidelidade para Selos Visuais de Assinatura PDF.
|
||||
* Gera imagens nítidas (300 DPI equivalente) baseadas nos Temas e vetores SVG,
|
||||
* utilizando exclusivamente a API gráfica padrão Java2D (AWT), sem dependência de JavaFX.
|
||||
*
|
||||
* Garante que o nome completo do signatário seja sempre exibido na íntegra,
|
||||
* além de respeitar as opções de CPF (completo, mascarado ou omitido) e data/hora.
|
||||
*/
|
||||
public final class PdfSealRenderer {
|
||||
|
||||
private PdfSealRenderer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza o selo visual baseado no tema e dados fornecidos para um BufferedImage em escala 2x (alta nitidez).
|
||||
*/
|
||||
public static BufferedImage renderToImage(PdfSealTheme theme, PdfSealData data) {
|
||||
Objects.requireNonNull(theme, "Tema não pode ser nulo.");
|
||||
Objects.requireNonNull(data, "Dados da assinatura não podem ser nulos.");
|
||||
|
||||
int baseWidth = theme.width();
|
||||
int baseHeight = theme.height();
|
||||
// Escala 2x para renderização super-nítida no documento PDF e na interface
|
||||
int scale = 2;
|
||||
int imgWidth = baseWidth * scale;
|
||||
int imgHeight = baseHeight * scale;
|
||||
|
||||
BufferedImage image = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
|
||||
try {
|
||||
// Configuração de anti-aliasing e renderização de alta qualidade
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
|
||||
g2d.scale(scale, scale);
|
||||
|
||||
Color primaryColor = parseColor(theme.primaryColor(), new Color(13, 71, 161));
|
||||
Color secondaryColor = parseColor(theme.secondaryColor(), new Color(2, 132, 199));
|
||||
Color bgColor = parseColor(theme.backgroundColor(), Color.WHITE);
|
||||
Color borderColor = parseColor(theme.borderColor(), new Color(203, 213, 225));
|
||||
Color textColor = parseColor(theme.textColor(), new Color(15, 23, 42));
|
||||
|
||||
// 1. Fundo do Selo com cantos arredondados
|
||||
g2d.setColor(bgColor);
|
||||
g2d.fill(new RoundRectangle2D.Float(1, 1, baseWidth - 2, baseHeight - 2, 8, 8));
|
||||
|
||||
// 2. Borda externa
|
||||
g2d.setColor(borderColor);
|
||||
g2d.setStroke(new BasicStroke(1.2f));
|
||||
g2d.draw(new RoundRectangle2D.Float(1, 1, baseWidth - 2, baseHeight - 2, 8, 8));
|
||||
|
||||
// 3. Tarja/Acento lateral do tema
|
||||
g2d.setColor(primaryColor);
|
||||
g2d.fill(new RoundRectangle2D.Float(1, 1, 5, baseHeight - 2, 3, 3));
|
||||
|
||||
if ("gov-br".equalsIgnoreCase(theme.id()) || "star".equalsIgnoreCase(theme.iconStyle())) {
|
||||
g2d.setColor(secondaryColor);
|
||||
g2d.fillRect(6, 1, 3, baseHeight - 2);
|
||||
}
|
||||
|
||||
// 4. Emblema à Esquerda (Logo Customizada em Imagem ou Ícone Vetorial)
|
||||
if (theme.customLogoBase64() != null && !theme.customLogoBase64().isBlank()) {
|
||||
drawCustomLogo(g2d, theme.customLogoBase64(), 10, 14, 36, baseHeight - 28);
|
||||
} else {
|
||||
String effectiveIcon = (theme.iconStyle() != null && !theme.iconStyle().isBlank()) ? theme.iconStyle() : theme.id();
|
||||
drawThemeIcon(g2d, effectiveIcon, primaryColor, secondaryColor);
|
||||
}
|
||||
|
||||
// 5. Informações Textuais da Assinatura (Focado no Titular, sem dados técnicos/emissor)
|
||||
int textX = 52;
|
||||
int textWidthAvailable = baseWidth - textX - 12;
|
||||
|
||||
String formattedDoc = formatSignerDoc(data);
|
||||
String customInfo = (data.customExtraInfo() != null && !data.customExtraInfo().isBlank())
|
||||
? data.customExtraInfo().trim()
|
||||
: theme.formatCustomField();
|
||||
|
||||
String identityLine;
|
||||
if (!formattedDoc.isBlank() && !customInfo.isBlank()) {
|
||||
identityLine = formattedDoc + " • " + customInfo;
|
||||
} else if (!formattedDoc.isBlank()) {
|
||||
identityLine = formattedDoc;
|
||||
} else {
|
||||
identityLine = customInfo;
|
||||
}
|
||||
|
||||
boolean hasIdentity = !identityLine.isBlank();
|
||||
boolean hasDate = data.includeDateTime();
|
||||
|
||||
String fullName = data.signerName() != null ? data.signerName().trim() : "Signatário ICP-Brasil";
|
||||
String headerText = resolveHeaderText(theme);
|
||||
String footerText = resolveFooterText(theme, null);
|
||||
|
||||
if (hasIdentity && hasDate) {
|
||||
// Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/Matrícula), Data, Rodapé
|
||||
g2d.setColor(primaryColor);
|
||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9));
|
||||
g2d.drawString(headerText, textX, 16);
|
||||
|
||||
g2d.setColor(textColor);
|
||||
drawFullSignerName(g2d, fullName, textX, 33, textWidthAvailable, 13.0f);
|
||||
|
||||
g2d.setColor(new Color(51, 65, 85));
|
||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
|
||||
drawFittedText(g2d, identityLine, textX, 47, textWidthAvailable, 10.0f);
|
||||
|
||||
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);
|
||||
|
||||
} else if (hasIdentity || hasDate) {
|
||||
// Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé
|
||||
g2d.setColor(primaryColor);
|
||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10));
|
||||
g2d.drawString(headerText, textX, 17);
|
||||
|
||||
g2d.setColor(textColor);
|
||||
drawFullSignerName(g2d, fullName, textX, 36, textWidthAvailable, 13.5f);
|
||||
|
||||
g2d.setColor(new Color(51, 65, 85));
|
||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
|
||||
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);
|
||||
|
||||
} else {
|
||||
// Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé
|
||||
g2d.setColor(primaryColor);
|
||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10));
|
||||
g2d.drawString(headerText, textX, 19);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha o logotipo personalizado fornecido em Base64, preservando o aspecto proporcional
|
||||
* e aplicando anti-aliasing e interpolação bilinear de alta fidelidade.
|
||||
*/
|
||||
private static void drawCustomLogo(Graphics2D g2d, String base64Data, int x, int y, int maxWidth, int maxHeight) {
|
||||
try {
|
||||
String cleanBase64 = base64Data;
|
||||
int commaIdx = cleanBase64.indexOf(",");
|
||||
if (commaIdx >= 0 && cleanBase64.substring(0, commaIdx).contains("base64")) {
|
||||
cleanBase64 = cleanBase64.substring(commaIdx + 1);
|
||||
}
|
||||
byte[] bytes = java.util.Base64.getDecoder().decode(cleanBase64.trim());
|
||||
BufferedImage logoImg = ImageIO.read(new java.io.ByteArrayInputStream(bytes));
|
||||
if (logoImg != null) {
|
||||
int originalW = logoImg.getWidth();
|
||||
int originalH = logoImg.getHeight();
|
||||
if (originalW > 0 && originalH > 0) {
|
||||
double scaleW = (double) maxWidth / originalW;
|
||||
double scaleH = (double) maxHeight / originalH;
|
||||
double scale = Math.min(scaleW, scaleH);
|
||||
int targetW = Math.max(1, (int) Math.round(originalW * scale));
|
||||
int targetH = Math.max(1, (int) Math.round(originalH * scale));
|
||||
|
||||
int posX = x + (maxWidth - targetW) / 2;
|
||||
int posY = y + (maxHeight - targetH) / 2;
|
||||
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.drawImage(logoImg, posX, posY, targetW, targetH, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Se houver falha na leitura do logo, recorre ao escudo padrão
|
||||
}
|
||||
drawThemeIcon(g2d, "shield", new Color(13, 71, 161), new Color(2, 132, 199));
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha o nome completo do signatário sem cortes ou truncamentos.
|
||||
* Ajusta a fonte gradativamente para caber perfeitamente na largura do selo.
|
||||
*/
|
||||
private static void drawFullSignerName(Graphics2D g2d, String fullName, int x, int y, int maxWidth, float preferredSize) {
|
||||
float fontSize = preferredSize;
|
||||
Font font = new Font(Font.SANS_SERIF, Font.BOLD, Math.round(fontSize));
|
||||
FontMetrics fm = g2d.getFontMetrics(font);
|
||||
|
||||
// Se o nome for longo, reduz o tamanho da fonte até caber perfeitamente
|
||||
while (fm.stringWidth(fullName) > maxWidth && fontSize > 8.0f) {
|
||||
fontSize -= 0.5f;
|
||||
font = new Font(Font.SANS_SERIF, Font.BOLD, Math.round(fontSize));
|
||||
fm = g2d.getFontMetrics(font);
|
||||
}
|
||||
|
||||
g2d.setFont(font);
|
||||
g2d.drawString(fullName, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Desenha texto informativo secundário ajustando suavemente a fonte caso exceda a largura.
|
||||
*/
|
||||
private static void drawFittedText(Graphics2D g2d, String text, int x, int y, int maxWidth, float baseSize) {
|
||||
if (text == null || text.isBlank()) return;
|
||||
float fontSize = baseSize;
|
||||
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(fontSize));
|
||||
FontMetrics fm = g2d.getFontMetrics(font);
|
||||
|
||||
while (fm.stringWidth(text) > maxWidth && fontSize > 7.5f) {
|
||||
fontSize -= 0.5f;
|
||||
font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(fontSize));
|
||||
fm = g2d.getFontMetrics(font);
|
||||
}
|
||||
|
||||
g2d.setFont(font);
|
||||
g2d.drawString(text, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() : "";
|
||||
|
||||
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", "");
|
||||
String formatted = mode.formatCpf(digits);
|
||||
return formatted.isBlank() ? "" : "CPF: " + formatted;
|
||||
}
|
||||
|
||||
return rawDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza o selo visual e o exporta diretamente como array de bytes PNG.
|
||||
*/
|
||||
public static byte[] renderToPngBytes(PdfSealTheme theme, PdfSealData data) throws IOException {
|
||||
BufferedImage img = renderToImage(theme, data);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "PNG", baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static void drawThemeIcon(Graphics2D g2d, String iconStyleOrId, Color primary, Color secondary) {
|
||||
g2d.translate(14, 18);
|
||||
|
||||
String style = (iconStyleOrId != null) ? iconStyleOrId.toLowerCase() : "shield";
|
||||
|
||||
if ("cybercore-modern".equals(style) || "crystal".equals(style)) {
|
||||
// Cristal CyberCore Vetorial
|
||||
Path2D.Float crystal = new Path2D.Float();
|
||||
crystal.moveTo(16, 2);
|
||||
crystal.lineTo(28, 9);
|
||||
crystal.lineTo(28, 25);
|
||||
crystal.lineTo(16, 32);
|
||||
crystal.lineTo(4, 25);
|
||||
crystal.lineTo(4, 9);
|
||||
crystal.closePath();
|
||||
|
||||
g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 35));
|
||||
g2d.fill(crystal);
|
||||
g2d.setColor(primary);
|
||||
g2d.setStroke(new BasicStroke(1.5f));
|
||||
g2d.draw(crystal);
|
||||
|
||||
g2d.setColor(secondary);
|
||||
g2d.drawLine(16, 2, 16, 32);
|
||||
g2d.setColor(primary);
|
||||
g2d.drawLine(4, 17, 28, 17);
|
||||
|
||||
} else if ("minimal-clean".equals(style) || "notary".equals(style) || "cartorio".equals(style)) {
|
||||
// Carimbo notarial sóbrio
|
||||
g2d.setColor(primary);
|
||||
g2d.setStroke(new BasicStroke(1.5f));
|
||||
g2d.draw(new Ellipse2D.Float(2, 2, 28, 28));
|
||||
g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f));
|
||||
g2d.draw(new Ellipse2D.Float(5, 5, 22, 22));
|
||||
|
||||
// Checkmark
|
||||
g2d.setStroke(new BasicStroke(1.8f));
|
||||
g2d.drawLine(10, 16, 14, 20);
|
||||
g2d.drawLine(14, 20, 22, 12);
|
||||
|
||||
} else if ("gov-br".equals(style) || "star".equals(style) || "governo".equals(style)) {
|
||||
// Brasão estelar estilizado
|
||||
g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 30));
|
||||
g2d.fill(new Ellipse2D.Float(2, 2, 28, 28));
|
||||
g2d.setColor(primary);
|
||||
g2d.setStroke(new BasicStroke(1.4f));
|
||||
g2d.draw(new Ellipse2D.Float(2, 2, 28, 28));
|
||||
|
||||
// Estrela interna
|
||||
Path2D.Float star = new Path2D.Float();
|
||||
star.moveTo(16, 6);
|
||||
star.lineTo(19, 13);
|
||||
star.lineTo(26, 13);
|
||||
star.lineTo(20, 17);
|
||||
star.lineTo(22, 24);
|
||||
star.lineTo(16, 20);
|
||||
star.lineTo(10, 24);
|
||||
star.lineTo(12, 17);
|
||||
star.lineTo(6, 13);
|
||||
star.lineTo(13, 13);
|
||||
star.closePath();
|
||||
g2d.setColor(secondary);
|
||||
g2d.fill(star);
|
||||
|
||||
} else {
|
||||
// Padrão: Escudo Criptográfico Oficial com Cadeado
|
||||
Path2D.Float shield = new Path2D.Float();
|
||||
shield.moveTo(16, 2);
|
||||
shield.lineTo(28, 6);
|
||||
shield.lineTo(28, 16);
|
||||
shield.curveTo(28, 25, 22, 31, 16, 34);
|
||||
shield.curveTo(10, 31, 4, 25, 4, 16);
|
||||
shield.lineTo(4, 6);
|
||||
shield.closePath();
|
||||
|
||||
g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 35));
|
||||
g2d.fill(shield);
|
||||
g2d.setColor(primary);
|
||||
g2d.setStroke(new BasicStroke(1.5f));
|
||||
g2d.draw(shield);
|
||||
|
||||
// Cadeado
|
||||
g2d.setColor(primary);
|
||||
g2d.fillRect(11, 15, 10, 8);
|
||||
g2d.setStroke(new BasicStroke(1.3f));
|
||||
g2d.drawArc(12, 9, 8, 8, 0, 180);
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillOval(15, 17, 2, 2);
|
||||
}
|
||||
|
||||
g2d.translate(-14, -18);
|
||||
}
|
||||
|
||||
private static String resolveHeaderText(PdfSealTheme theme) {
|
||||
if (theme.customHeader() != null && !theme.customHeader().isBlank()) {
|
||||
return theme.customHeader().toUpperCase();
|
||||
}
|
||||
return switch (theme.id().toLowerCase()) {
|
||||
case "cybercore-modern" -> "CYBERCORE SIGNER • AUTENTICAÇÃO DIGITAL";
|
||||
case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE";
|
||||
case "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001";
|
||||
default -> "ASSINADO DIGITALMENTE • ICP-BRASIL";
|
||||
};
|
||||
}
|
||||
|
||||
private static String resolveFooterText(PdfSealTheme theme, String hash) {
|
||||
if (theme.customFooter() != null && !theme.customFooter().isBlank()) {
|
||||
return theme.customFooter();
|
||||
}
|
||||
if (hash != null && !hash.isBlank()) {
|
||||
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";
|
||||
};
|
||||
}
|
||||
|
||||
private static Color parseColor(String hex, Color def) {
|
||||
if (hex == null || !hex.startsWith("#")) return def;
|
||||
try {
|
||||
return Color.decode(hex);
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package br.com.cybercore.signer.core.pdf.seal;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Modelo de configuração e identidade visual de um Tema de Selo de Assinatura PDF.
|
||||
* Permite definir paleta de cores, dimensões, templates, textos personalizados e preferências de exibição.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record PdfSealTheme(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
int width,
|
||||
int height,
|
||||
String primaryColor,
|
||||
String secondaryColor,
|
||||
String backgroundColor,
|
||||
String borderColor,
|
||||
String textColor,
|
||||
String svgTemplate,
|
||||
String customHeader,
|
||||
String customFooter,
|
||||
String iconStyle,
|
||||
CpfDisplayMode defaultCpfMode,
|
||||
boolean defaultIncludeDateTime,
|
||||
boolean isCustom,
|
||||
String customLogoBase64,
|
||||
String customFieldLabel,
|
||||
String customFieldValue
|
||||
) {
|
||||
|
||||
public PdfSealTheme {
|
||||
Objects.requireNonNull(id, "ID do tema não pode ser nulo.");
|
||||
Objects.requireNonNull(name, "Nome do tema não pode ser nulo.");
|
||||
if (width <= 0) width = 320;
|
||||
if (height <= 0) height = 82;
|
||||
if (primaryColor == null) primaryColor = "#0d47a1";
|
||||
if (secondaryColor == null) secondaryColor = "#0284c7";
|
||||
if (backgroundColor == null) backgroundColor = "#ffffff";
|
||||
if (borderColor == null) borderColor = "#cbd5e1";
|
||||
if (textColor == null) textColor = "#0f172a";
|
||||
if (iconStyle == null || iconStyle.isBlank()) iconStyle = "shield";
|
||||
if (defaultCpfMode == null) defaultCpfMode = CpfDisplayMode.FULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor para temas customizados com logotipo Base64, sem campos extras.
|
||||
*/
|
||||
public PdfSealTheme(String id, String name, String description, int width, int height,
|
||||
String primaryColor, String secondaryColor, String backgroundColor,
|
||||
String borderColor, String textColor, String svgTemplate,
|
||||
String customHeader, String customFooter, String iconStyle,
|
||||
CpfDisplayMode defaultCpfMode, boolean defaultIncludeDateTime, boolean isCustom,
|
||||
String customLogoBase64) {
|
||||
this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor,
|
||||
borderColor, textColor, svgTemplate, customHeader, customFooter, iconStyle,
|
||||
defaultCpfMode, defaultIncludeDateTime, isCustom, customLogoBase64, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor para temas customizados sem logo personalizada em imagem.
|
||||
*/
|
||||
public PdfSealTheme(String id, String name, String description, int width, int height,
|
||||
String primaryColor, String secondaryColor, String backgroundColor,
|
||||
String borderColor, String textColor, String svgTemplate,
|
||||
String customHeader, String customFooter, String iconStyle,
|
||||
CpfDisplayMode defaultCpfMode, boolean defaultIncludeDateTime, boolean isCustom) {
|
||||
this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor,
|
||||
borderColor, textColor, svgTemplate, customHeader, customFooter, iconStyle,
|
||||
defaultCpfMode, defaultIncludeDateTime, isCustom, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construtor legado para os temas embutidos do sistema.
|
||||
*/
|
||||
public PdfSealTheme(String id, String name, String description, int width, int height,
|
||||
String primaryColor, String secondaryColor, String backgroundColor,
|
||||
String borderColor, String textColor, String svgTemplate) {
|
||||
this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor,
|
||||
borderColor, textColor, svgTemplate, null, null, "shield", CpfDisplayMode.FULL, true, false, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata o campo customizado (ex: "Matrícula: 104829" ou "Cargo: Auditor Fiscal").
|
||||
*/
|
||||
public String formatCustomField() {
|
||||
boolean hasLabel = customFieldLabel != null && !customFieldLabel.isBlank();
|
||||
boolean hasValue = customFieldValue != null && !customFieldValue.isBlank();
|
||||
if (hasLabel && hasValue) {
|
||||
String label = customFieldLabel.trim();
|
||||
if (!label.endsWith(":")) label += ":";
|
||||
return label + " " + customFieldValue.trim();
|
||||
} else if (hasValue) {
|
||||
return customFieldValue.trim();
|
||||
} else if (hasLabel) {
|
||||
return customFieldLabel.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera o código SVG final substituindo os marcadores pelos dados reais da assinatura.
|
||||
*/
|
||||
public String renderSvg(PdfSealData data) {
|
||||
if (svgTemplate == null || svgTemplate.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return svgTemplate
|
||||
.replace("${THEME_PRIMARY}", primaryColor)
|
||||
.replace("${THEME_SECONDARY}", secondaryColor)
|
||||
.replace("${THEME_BG}", backgroundColor)
|
||||
.replace("${THEME_BORDER}", borderColor)
|
||||
.replace("${THEME_TEXT}", textColor)
|
||||
.replace("${SIGNER_NAME}", escapeXml(data.signerName()))
|
||||
.replace("${SIGNER_DOC}", escapeXml(data.identifier()))
|
||||
.replace("${SIGNING_DATE}", escapeXml(data.signingDate()))
|
||||
.replace("${CERT_ISSUER}", escapeXml(data.issuer()))
|
||||
.replace("${HASH_SUMMARY}", escapeXml(data.hashSummary()))
|
||||
.replace("${REASON}", escapeXml(data.reason()))
|
||||
.replace("${LOCATION}", escapeXml(data.location()));
|
||||
}
|
||||
|
||||
private static String escapeXml(String text) {
|
||||
if (text == null) return "";
|
||||
return text
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package br.com.cybercore.signer.core.pdf.seal;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Registro central de temas de selos visuais para assinatura PDF.
|
||||
* Inclui os temas padrão do sistema e permite instalar, gerenciar e persistir dinamicamente
|
||||
* temas personalizados de selo criados ou importados pelo usuário.
|
||||
*/
|
||||
public final class PdfSealThemeRegistry {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PdfSealThemeRegistry.class);
|
||||
|
||||
private static final Map<String, PdfSealTheme> THEMES = new ConcurrentHashMap<>();
|
||||
private static final CustomSealStorage STORAGE = new CustomSealStorage();
|
||||
|
||||
public static final String DEFAULT_THEME_ID = "icp-brasil";
|
||||
|
||||
static {
|
||||
registerBuiltInThemes();
|
||||
loadCustomThemes();
|
||||
}
|
||||
|
||||
private PdfSealThemeRegistry() {
|
||||
}
|
||||
|
||||
private static void registerBuiltInThemes() {
|
||||
// 1. Tema ICP-Brasil Institucional
|
||||
registerTheme(new PdfSealTheme(
|
||||
"icp-brasil",
|
||||
"ICP-Brasil Institucional",
|
||||
"Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
|
||||
320,
|
||||
82,
|
||||
"#0d47a1",
|
||||
"#0284c7",
|
||||
"#ffffff",
|
||||
"#93c5fd",
|
||||
"#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="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 -->
|
||||
<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"/>
|
||||
<!-- Cadeado -->
|
||||
<rect x="12" y="17" width="12" height="10" rx="2" fill="${THEME_PRIMARY}"/>
|
||||
<path d="M14 17 V13 C14 10.8 15.8 9 18 9 C20.2 9 22 10.8 22 13 V17" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.8"/>
|
||||
<circle cx="18" cy="22" r="1.5" fill="#ffffff"/>
|
||||
</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}" letter-spacing="0.5">ASSINADO DIGITALMENTE • ICP-BRASIL</text>
|
||||
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
|
||||
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER}</text>
|
||||
<text x="56" y="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>
|
||||
</svg>
|
||||
"""
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega os temas personalizados armazenados no disco.
|
||||
*/
|
||||
public static void loadCustomThemes() {
|
||||
try {
|
||||
List<PdfSealTheme> customs = STORAGE.loadAllCustomSeals();
|
||||
for (PdfSealTheme theme : customs) {
|
||||
THEMES.put(theme.id().toLowerCase(), theme);
|
||||
}
|
||||
logger.info("Carregados {} temas de selos personalizados do disco.", customs.size());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Não foi possível carregar temas personalizados: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerTheme(PdfSealTheme theme) {
|
||||
Objects.requireNonNull(theme, "Tema não pode ser nulo.");
|
||||
THEMES.put(theme.id().toLowerCase(), theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instala um tema personalizado, salvando-o no disco e registrando-o para uso imediato.
|
||||
*/
|
||||
public static void installCustomTheme(PdfSealTheme theme) throws IOException {
|
||||
Objects.requireNonNull(theme, "Tema não pode ser nulo.");
|
||||
STORAGE.saveSeal(theme);
|
||||
registerTheme(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um tema personalizado do disco e do registro.
|
||||
* Não permite remover temas padrão do sistema.
|
||||
*/
|
||||
public static boolean uninstallCustomTheme(String themeId) {
|
||||
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.");
|
||||
}
|
||||
|
||||
boolean deleted = STORAGE.deleteSeal(idLower);
|
||||
THEMES.remove(idLower);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public static Optional<PdfSealTheme> findById(String id) {
|
||||
if (id == null) return Optional.empty();
|
||||
return Optional.ofNullable(THEMES.get(id.toLowerCase()));
|
||||
}
|
||||
|
||||
public static Optional<PdfSealTheme> getTheme(String id) {
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
public static PdfSealTheme getDefaultTheme() {
|
||||
return THEMES.getOrDefault(DEFAULT_THEME_ID, THEMES.values().iterator().next());
|
||||
}
|
||||
|
||||
public static List<PdfSealTheme> listThemes() {
|
||||
List<PdfSealTheme> list = new ArrayList<>(THEMES.values());
|
||||
return Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
public static CustomSealStorage getStorage() {
|
||||
return STORAGE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
/**
|
||||
* Códigos de erro padronizados do protocolo CyberCore Signing Protocol (CCSP/1).
|
||||
*/
|
||||
public enum CcspErrorCode {
|
||||
CERTIFICATE_NOT_FOUND("Certificado especificado não foi encontrado."),
|
||||
CERTIFICATE_EXPIRED("O certificado selecionado está expirado."),
|
||||
CERTIFICATE_INVALID("O certificado é inválido ou está corrompido."),
|
||||
PRIVATE_KEY_NOT_AVAILABLE("Chave privada inacessível ou não disponível para a operação."),
|
||||
TOKEN_NOT_FOUND("Nenhum token ou leitor de smart card compatível foi encontrado."),
|
||||
TOKEN_REMOVED("O token ou smart card foi desconectado durante a operação."),
|
||||
PIN_REQUIRED("PIN obrigatório para desbloqueio do dispositivo."),
|
||||
PIN_INCORRECT("PIN informado é incorreto."),
|
||||
PKCS11_ERROR("Falha na comunicação com o módulo PKCS#11 do dispositivo."),
|
||||
SIGNATURE_FAILED("Falha ao gerar a assinatura digital."),
|
||||
APPLICATION_NOT_AUTHORIZED("Aplicação web não possui autorização para solicitar assinaturas."),
|
||||
REQUEST_EXPIRED("A requisição de assinatura expirou."),
|
||||
REPLAY_DETECTED("Requisição duplicada detectada (replay attack prevenido)."),
|
||||
INVALID_ORIGIN("Origem da requisição não é permitida para esta aplicação."),
|
||||
UNSUPPORTED_ALGORITHM("Algoritmo de assinatura não suportado."),
|
||||
USER_CANCELLED("Operação cancelada pelo usuário."),
|
||||
PROVIDER_NOT_AVAILABLE("O provedor criptográfico necessário não está disponível."),
|
||||
COMMUNICATION_ERROR("Erro interno de comunicação no agente local.");
|
||||
|
||||
private final String defaultMessage;
|
||||
|
||||
CcspErrorCode(String defaultMessage) {
|
||||
this.defaultMessage = defaultMessage;
|
||||
}
|
||||
|
||||
public String getDefaultMessage() {
|
||||
return defaultMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Operações suportadas pelo protocolo CCSP.
|
||||
*/
|
||||
public enum CcspOperation {
|
||||
SIGN("sign"),
|
||||
AUTHENTICATE("authenticate"),
|
||||
LIST_CERTIFICATES("list_certificates"),
|
||||
STATUS("status"),
|
||||
PING("ping");
|
||||
|
||||
private final String value;
|
||||
|
||||
CcspOperation(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static CcspOperation fromValue(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (CcspOperation op : values()) {
|
||||
if (op.value.equalsIgnoreCase(value.trim())) {
|
||||
return op;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Operação CCSP desconhecida: " + value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Mensagem de requisição do CyberCore Signing Protocol (CCSP/1).
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record CcspRequest(
|
||||
@JsonProperty("version")
|
||||
String version,
|
||||
|
||||
@JsonProperty("request_id")
|
||||
String requestId,
|
||||
|
||||
@JsonProperty("application_id")
|
||||
String applicationId,
|
||||
|
||||
@JsonProperty("operation")
|
||||
String operation,
|
||||
|
||||
@JsonProperty("algorithm")
|
||||
String algorithm,
|
||||
|
||||
@JsonProperty("digest")
|
||||
String digest,
|
||||
|
||||
@JsonProperty("certificate_hint")
|
||||
String certificateHint,
|
||||
|
||||
@JsonProperty("expires_at")
|
||||
String expiresAt,
|
||||
|
||||
@JsonProperty("timestamp")
|
||||
Long timestamp,
|
||||
|
||||
@JsonProperty("nonce")
|
||||
String nonce,
|
||||
|
||||
@JsonProperty("signature_type")
|
||||
String signatureType,
|
||||
|
||||
@JsonProperty("document_name")
|
||||
String documentName,
|
||||
|
||||
@JsonProperty("document_type")
|
||||
String documentType,
|
||||
|
||||
@JsonProperty("purpose")
|
||||
String purpose,
|
||||
|
||||
@JsonProperty("challenge")
|
||||
String challenge
|
||||
) implements Serializable {
|
||||
|
||||
public static final String PROTOCOL_VERSION = "1";
|
||||
|
||||
public CcspRequest(
|
||||
String version, String requestId, String applicationId, String operation,
|
||||
String algorithm, String digest, String certificateHint, String expiresAt,
|
||||
Long timestamp, String nonce, String signatureType
|
||||
) {
|
||||
this(version, requestId, applicationId, operation, algorithm, digest,
|
||||
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
||||
null, null, null, null);
|
||||
}
|
||||
|
||||
public boolean isVersionSupported() {
|
||||
return PROTOCOL_VERSION.equals(version);
|
||||
}
|
||||
|
||||
public boolean isAuthentication() {
|
||||
return "authenticate".equalsIgnoreCase(operation)
|
||||
|| "AUTHENTICATION".equalsIgnoreCase(purpose)
|
||||
|| "AUTH_CHALLENGE".equalsIgnoreCase(documentType)
|
||||
|| (challenge != null && !challenge.isBlank());
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
if (expiresAt == null || expiresAt.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Instant expiry = Instant.parse(expiresAt);
|
||||
return Instant.now().isAfter(expiry);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mensagem de resposta do CyberCore Signing Protocol (CCSP/1).
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record CcspResponse(
|
||||
@JsonProperty("version")
|
||||
String version,
|
||||
|
||||
@JsonProperty("request_id")
|
||||
String requestId,
|
||||
|
||||
@JsonProperty("status")
|
||||
String status,
|
||||
|
||||
@JsonProperty("signature")
|
||||
String signature,
|
||||
|
||||
@JsonProperty("certificate")
|
||||
String certificate,
|
||||
|
||||
@JsonProperty("certificate_chain")
|
||||
List<String> certificateChain,
|
||||
|
||||
@JsonProperty("algorithm")
|
||||
String algorithm,
|
||||
|
||||
@JsonProperty("error_code")
|
||||
String errorCode,
|
||||
|
||||
@JsonProperty("message")
|
||||
String message,
|
||||
|
||||
@JsonProperty("subject_name")
|
||||
String subjectName,
|
||||
|
||||
@JsonProperty("subject_cpf_cnpj")
|
||||
String subjectCpfCnpj,
|
||||
|
||||
@JsonProperty("document_name")
|
||||
String documentName,
|
||||
|
||||
@JsonProperty("document_type")
|
||||
String documentType
|
||||
) implements Serializable {
|
||||
|
||||
public CcspResponse {
|
||||
certificateChain = certificateChain != null ? List.copyOf(certificateChain) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> certificateChain() {
|
||||
return certificateChain != null ? Collections.unmodifiableList(certificateChain) : null;
|
||||
}
|
||||
|
||||
public static final String STATUS_SIGNED = "signed";
|
||||
public static final String STATUS_AUTHENTICATED = "authenticated";
|
||||
public static final String STATUS_SUCCESS = "success";
|
||||
public static final String STATUS_ERROR = "error";
|
||||
|
||||
public static CcspResponse success(String requestId, String signatureBase64, String certBase64,
|
||||
List<String> chain, String algorithm) {
|
||||
return success(requestId, signatureBase64, certBase64, chain, algorithm, null, null, null, null);
|
||||
}
|
||||
|
||||
public static CcspResponse success(String requestId, String signatureBase64, String certBase64,
|
||||
List<String> chain, String algorithm,
|
||||
String subjectName, String subjectCpfCnpj,
|
||||
String documentName, String documentType) {
|
||||
return new CcspResponse(
|
||||
CcspRequest.PROTOCOL_VERSION,
|
||||
requestId,
|
||||
STATUS_SIGNED,
|
||||
signatureBase64,
|
||||
certBase64,
|
||||
chain,
|
||||
algorithm,
|
||||
null,
|
||||
null,
|
||||
subjectName,
|
||||
subjectCpfCnpj,
|
||||
documentName,
|
||||
documentType
|
||||
);
|
||||
}
|
||||
|
||||
public static CcspResponse authenticated(String requestId, String signatureBase64, String certBase64,
|
||||
List<String> chain, String algorithm,
|
||||
String subjectName, String subjectCpfCnpj) {
|
||||
return new CcspResponse(
|
||||
CcspRequest.PROTOCOL_VERSION,
|
||||
requestId,
|
||||
STATUS_AUTHENTICATED,
|
||||
signatureBase64,
|
||||
certBase64,
|
||||
chain,
|
||||
algorithm,
|
||||
null,
|
||||
null,
|
||||
subjectName,
|
||||
subjectCpfCnpj,
|
||||
null,
|
||||
"AUTH_CHALLENGE"
|
||||
);
|
||||
}
|
||||
|
||||
public static CcspResponse error(String requestId, CcspErrorCode errorCode, String message) {
|
||||
return new CcspResponse(
|
||||
CcspRequest.PROTOCOL_VERSION,
|
||||
requestId,
|
||||
STATUS_ERROR,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
errorCode.name(),
|
||||
message != null ? message : errorCode.getDefaultMessage(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static CcspResponse error(String requestId, CcspErrorCode errorCode) {
|
||||
return error(requestId, errorCode, errorCode.getDefaultMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
|
||||
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.SigningResult;
|
||||
import java.security.MessageDigest;
|
||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||
import br.com.cybercore.signer.core.security.ApplicationRegistry;
|
||||
import br.com.cybercore.signer.core.security.AuthorizedApplication;
|
||||
import br.com.cybercore.signer.core.security.NonceManager;
|
||||
import br.com.cybercore.signer.core.security.OriginValidator;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Servidor HTTP local do CyberCore Signing Protocol (CCSP/1).
|
||||
* Vinculado exclusivamente ao loopback 127.0.0.1 para comunicação segura com navegadores web.
|
||||
*/
|
||||
public class CcspServer {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CcspServer.class);
|
||||
public static final int DEFAULT_PORT = 21325;
|
||||
public static final String BIND_HOST = "127.0.0.1";
|
||||
|
||||
private final int port;
|
||||
private final CertificateManager certificateManager;
|
||||
private final SigningService signingService;
|
||||
private final ApplicationRegistry applicationRegistry;
|
||||
private final NonceManager nonceManager;
|
||||
private final OriginValidator originValidator;
|
||||
private final CcspUserPromptListener promptListener;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ReentrantLock signLock = new ReentrantLock(true); // Concorrência FIFO
|
||||
|
||||
private HttpServer httpServer;
|
||||
private ExecutorService executor;
|
||||
private volatile boolean running = false;
|
||||
|
||||
public CcspServer(int port,
|
||||
CertificateManager certificateManager,
|
||||
SigningService signingService,
|
||||
ApplicationRegistry applicationRegistry,
|
||||
CcspUserPromptListener promptListener) {
|
||||
this.port = port;
|
||||
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
|
||||
this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null");
|
||||
this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
|
||||
this.promptListener = promptListener;
|
||||
this.nonceManager = new NonceManager();
|
||||
this.originValidator = new OriginValidator();
|
||||
this.objectMapper = new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
}
|
||||
|
||||
public CcspServer(CertificateManager certificateManager,
|
||||
SigningService signingService,
|
||||
CcspUserPromptListener promptListener) {
|
||||
this(DEFAULT_PORT, certificateManager, signingService, ApplicationRegistry.getInstance(), promptListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicia o servidor local na porta configurada.
|
||||
*/
|
||||
public synchronized void start() throws IOException {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress(BIND_HOST, port);
|
||||
httpServer = HttpServer.create(address, 50);
|
||||
|
||||
executor = Executors.newFixedThreadPool(8, r -> {
|
||||
Thread t = new Thread(r, "ccsp-http-worker");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
httpServer.setExecutor(executor);
|
||||
|
||||
// Registra endpoints do protocolo CCSP/1
|
||||
httpServer.createContext("/ccsp/v1/status", new StatusHandler());
|
||||
httpServer.createContext("/ccsp/v1/certificates", new CertificatesHandler());
|
||||
httpServer.createContext("/ccsp/v1/sign", new SignHandler());
|
||||
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
|
||||
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler());
|
||||
|
||||
httpServer.start();
|
||||
running = true;
|
||||
logger.info("Servidor local CCSP/1 iniciado com sucesso em http://{}:{}", BIND_HOST, getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* Encerra a execução do servidor local.
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpServer != null) {
|
||||
httpServer.stop(1);
|
||||
httpServer = null;
|
||||
}
|
||||
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
executor = null;
|
||||
}
|
||||
|
||||
running = false;
|
||||
logger.info("Servidor local CCSP/1 finalizado.");
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
if (httpServer != null) {
|
||||
return httpServer.getAddress().getPort();
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Handlers HTTP
|
||||
// =========================================================================
|
||||
|
||||
private class StatusHandler 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;
|
||||
}
|
||||
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("version", CcspRequest.PROTOCOL_VERSION);
|
||||
resp.put("status", "ready");
|
||||
resp.put("agent", "CyberCore Signer");
|
||||
resp.put("agent_version", "0.1.0");
|
||||
resp.put("os", System.getProperty("os.name"));
|
||||
resp.put("timestamp", Instant.now().toString());
|
||||
|
||||
sendJsonResponse(exchange, 200, resp);
|
||||
}
|
||||
}
|
||||
|
||||
private class CertificatesHandler 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;
|
||||
}
|
||||
|
||||
String origin = getOrigin(exchange);
|
||||
String appId = exchange.getRequestHeaders().getFirst("X-CyberCore-AppId");
|
||||
if (appId == null || appId.isBlank()) {
|
||||
URI uri = exchange.getRequestURI();
|
||||
appId = getQueryParam(uri, "application_id");
|
||||
}
|
||||
|
||||
// Verifica autorização se app_id e origin forem informados
|
||||
if (appId != null && !appId.isBlank()) {
|
||||
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
||||
if (!authorized) {
|
||||
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
|
||||
if (!approved) {
|
||||
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||
"Aplicação não autorizada pelo usuário.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<ManagedCertificate> certs = certificateManager.getAvailableCertificates();
|
||||
if (certs.isEmpty()) {
|
||||
certs = certificateManager.discoverCertificates();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> certDtoList = certs.stream().map(c -> {
|
||||
Map<String, Object> dto = new HashMap<>();
|
||||
dto.put("id", c.id());
|
||||
dto.put("alias", c.alias());
|
||||
dto.put("common_name", c.commonName());
|
||||
dto.put("subject_dn", c.subjectDn());
|
||||
dto.put("issuer_dn", c.issuerDn());
|
||||
dto.put("type", c.type().name());
|
||||
dto.put("provider_id", c.providerId());
|
||||
dto.put("device_name", c.hardwareDeviceName());
|
||||
dto.put("valid_from", c.notBefore() != null ? c.notBefore().toString() : null);
|
||||
dto.put("valid_to", c.notAfter() != null ? c.notAfter().toString() : null);
|
||||
dto.put("serial_number", c.serialNumber());
|
||||
dto.put("can_sign", c.canSign());
|
||||
dto.put("status", c.status().name());
|
||||
return dto;
|
||||
}).toList();
|
||||
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("version", CcspRequest.PROTOCOL_VERSION);
|
||||
resp.put("status", "success");
|
||||
resp.put("certificates", certDtoList);
|
||||
|
||||
sendJsonResponse(exchange, 200, resp);
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthorizeHandler 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);
|
||||
try (InputStream is = exchange.getRequestBody()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> body = objectMapper.readValue(is, Map.class);
|
||||
String appId = body.get("application_id");
|
||||
String appName = body.getOrDefault("name", "Aplicação Web");
|
||||
|
||||
if (appId == null || appId.isBlank()) {
|
||||
sendErrorResponse(exchange, 400, "BAD_REQUEST", "application_id é obrigatório.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (applicationRegistry.isAuthorized(appId, origin)) {
|
||||
Map<String, Object> resp = Map.of(
|
||||
"version", CcspRequest.PROTOCOL_VERSION,
|
||||
"status", "authorized",
|
||||
"application_id", appId
|
||||
);
|
||||
sendJsonResponse(exchange, 200, resp);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean approved = promptApplicationAuthorization(origin, appName, appId);
|
||||
if (approved) {
|
||||
Map<String, Object> resp = Map.of(
|
||||
"version", CcspRequest.PROTOCOL_VERSION,
|
||||
"status", "authorized",
|
||||
"application_id", appId
|
||||
);
|
||||
sendJsonResponse(exchange, 200, resp);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||
"Autorização negada pelo usuário.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 400, "BAD_REQUEST", "Payload JSON inválido: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SignHandler 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;
|
||||
}
|
||||
|
||||
// 1. Validação de versão
|
||||
if (!request.isVersionSupported()) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
|
||||
"Versão do protocolo não suportada: " + request.version());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Validação de expiração
|
||||
if (request.isExpired()) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED,
|
||||
"A requisição de assinatura expirou.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Validação anti-replay (Nonce)
|
||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Validação de autorização da aplicação
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Validação de digest / challenge
|
||||
byte[] digestBytes;
|
||||
if (request.digest() != null && !request.digest().isBlank()) {
|
||||
try {
|
||||
digestBytes = Base64.getDecoder().decode(request.digest());
|
||||
} catch (Exception e) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Digest em Base64 inválido.");
|
||||
return;
|
||||
}
|
||||
} else if (request.challenge() != null && !request.challenge().isBlank()) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
digestBytes = md.digest(request.challenge().getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Falha ao gerar digest do desafio: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||
"É obrigatório informar 'digest' ou 'challenge' para a operação.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Resolução do certificado
|
||||
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();
|
||||
|
||||
// 7. Confirmação do usuário na UI (se listener configurado)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Assinatura com controle sequencial FIFO
|
||||
signLock.lock();
|
||||
try {
|
||||
SigningContext ctx = new SigningContext(
|
||||
request.requestId(),
|
||||
request.applicationId(),
|
||||
digestBytes,
|
||||
request.algorithm() != null ? request.algorithm() : "SHA256withRSA",
|
||||
cert.id(),
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
SigningResult result = signingService.sign(ctx);
|
||||
String sigBase64 = Base64.getEncoder().encodeToString(result.signatureBytes());
|
||||
|
||||
String subjectName = X509CertificateUtils.extractHolderName(cert.commonName());
|
||||
String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj();
|
||||
boolean isAuth = request.isAuthentication() || exchange.getRequestURI().getPath().endsWith("/authenticate");
|
||||
|
||||
CcspResponse response = isAuth
|
||||
? CcspResponse.authenticated(
|
||||
request.requestId(),
|
||||
sigBase64,
|
||||
result.certificateBase64(),
|
||||
result.certificateChainBase64(),
|
||||
result.algorithm(),
|
||||
subjectName,
|
||||
cpfCnpj
|
||||
)
|
||||
: CcspResponse.success(
|
||||
request.requestId(),
|
||||
sigBase64,
|
||||
result.certificateBase64(),
|
||||
result.certificateChainBase64(),
|
||||
result.algorithm(),
|
||||
subjectName,
|
||||
cpfCnpj,
|
||||
request.documentName(),
|
||||
request.documentType()
|
||||
);
|
||||
|
||||
sendJsonResponse(exchange, 200, response);
|
||||
|
||||
} catch (CertificateProviderException cpe) {
|
||||
sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro inesperado na assinatura: {}", e.getMessage(), e);
|
||||
sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Erro interno: " + e.getMessage());
|
||||
} finally {
|
||||
signLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Utilitários de Segurança e HTTP
|
||||
// =========================================================================
|
||||
|
||||
private boolean promptApplicationAuthorization(String origin, String appName, String appId) {
|
||||
if (promptListener == null) {
|
||||
// Em testes unitários sem UI, aprova e registra
|
||||
applicationRegistry.register(new AuthorizedApplication(
|
||||
appId, appName, origin != null ? Set.of(origin) : Set.of("*"), true, Instant.now()
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
CompletableFuture<Boolean> future = promptListener.requestApplicationAuthorization(origin, appName, appId);
|
||||
Boolean approved = future.get(30, TimeUnit.SECONDS);
|
||||
if (Boolean.TRUE.equals(approved)) {
|
||||
applicationRegistry.register(new AuthorizedApplication(
|
||||
appId, appName, origin != null ? Set.of(origin) : Set.of("*"), true, Instant.now()
|
||||
));
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Timeout ou erro na autorização da aplicação: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleCors(HttpExchange exchange) throws IOException {
|
||||
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();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getOrigin(HttpExchange exchange) {
|
||||
String origin = exchange.getRequestHeaders().getFirst("Origin");
|
||||
if (origin == null || origin.isBlank()) {
|
||||
origin = exchange.getRequestHeaders().getFirst("Referer");
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
|
||||
private String getQueryParam(URI uri, String key) {
|
||||
if (uri.getQuery() == null) return null;
|
||||
for (String param : uri.getQuery().split("&")) {
|
||||
String[] pair = param.split("=");
|
||||
if (pair.length == 2 && pair[0].equalsIgnoreCase(key)) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void sendJsonResponse(HttpExchange exchange, int statusCode, Object data) throws IOException {
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(data);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException {
|
||||
Map<String, Object> err = Map.of(
|
||||
"version", CcspRequest.PROTOCOL_VERSION,
|
||||
"status", "error",
|
||||
"error_code", code,
|
||||
"message", message
|
||||
);
|
||||
sendJsonResponse(exchange, httpStatus, err);
|
||||
}
|
||||
|
||||
private void sendCcspError(HttpExchange exchange, int httpStatus, String reqId, CcspErrorCode code, String msg) throws IOException {
|
||||
CcspResponse err = CcspResponse.error(reqId, code, msg != null ? msg : code.getDefaultMessage());
|
||||
sendJsonResponse(exchange, httpStatus, err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package br.com.cybercore.signer.core.protocol;
|
||||
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Interface de interação com o usuário para autorização de aplicações e confirmação de assinaturas.
|
||||
* Permite desacoplar o servidor HTTP de comunicação da interface gráfica desktop (JavaFX).
|
||||
*/
|
||||
public interface CcspUserPromptListener {
|
||||
|
||||
/**
|
||||
* Solicita ao usuário autorização para que uma nova aplicação web se comunique com o agente.
|
||||
*
|
||||
* @param origin Origem HTTP/HTTPS da aplicação (ex: https://sistema.exemplo.com.br).
|
||||
* @param appName Nome amigável da aplicação solicitante.
|
||||
* @param applicationId ID único da aplicação.
|
||||
* @return Future completado com {@code true} se o usuário autorizar; {@code false} caso recuse.
|
||||
*/
|
||||
CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId);
|
||||
|
||||
/**
|
||||
* Solicita ao usuário confirmação visual explícita antes de realizar uma assinatura digital.
|
||||
*
|
||||
* @param request Requisição CCSP de assinatura recebida.
|
||||
* @param certificate Certificado digital selecionado para a operação.
|
||||
* @return Future completado com {@code true} se o usuário confirmar; {@code false} caso cancele.
|
||||
*/
|
||||
CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Gerenciador central de certificados digitais do CyberCore Signer.
|
||||
* Responsável pela descoberta em múltiplos provedores (Windows Certificate Store, A1, PKCS#11),
|
||||
* validação de integridade, mapeamento de provedores e seleção segura para operações de assinatura.
|
||||
*/
|
||||
public class CertificateManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CertificateManager.class);
|
||||
|
||||
private final List<CertificateProvider> providers = new CopyOnWriteArrayList<>();
|
||||
private final Map<String, CertificateProvider> certificateToProviderMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, ManagedCertificate> cachedCertificates = new ConcurrentHashMap<>();
|
||||
|
||||
public CertificateManager() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
||||
*/
|
||||
public static CertificateManager createDefault() {
|
||||
CertificateManager manager = new CertificateManager();
|
||||
manager.registerProvider(new WindowsCertificateStoreProvider());
|
||||
manager.registerProvider(new Pkcs12CertificateProvider());
|
||||
manager.registerProvider(new Pkcs11CertificateProvider());
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra um provedor de certificados no ecossistema do agente.
|
||||
*/
|
||||
public void registerProvider(CertificateProvider provider) {
|
||||
Objects.requireNonNull(provider, "Provedor não pode ser nulo.");
|
||||
// Evita duplicidade por providerId
|
||||
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(provider.getProviderId()));
|
||||
providers.add(provider);
|
||||
logger.info("Provedor registrado no CertificateManager: {} [{}] (disponível={})",
|
||||
provider.getDisplayName(), provider.getProviderId(), provider.isAvailable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove um provedor previamente registrado.
|
||||
*/
|
||||
public void unregisterProvider(String providerId) {
|
||||
if (providerId != null) {
|
||||
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
|
||||
certificateToProviderMap.values().removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
|
||||
}
|
||||
}
|
||||
|
||||
public List<CertificateProvider> getProviders() {
|
||||
return Collections.unmodifiableList(providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa a descoberta de certificados em todos os provedores operacionais.
|
||||
* Consolida os resultados, atualiza o mapeamento de provedores e ordena por relevância/disponibilidade.
|
||||
*/
|
||||
public synchronized List<ManagedCertificate> discoverCertificates() {
|
||||
logger.info("Iniciando descoberta de certificados digitais nos provedores ativos...");
|
||||
|
||||
certificateToProviderMap.clear();
|
||||
cachedCertificates.clear();
|
||||
List<ManagedCertificate> discovered = new ArrayList<>();
|
||||
|
||||
for (CertificateProvider provider : providers) {
|
||||
if (!provider.isAvailable()) {
|
||||
logger.debug("Provedor '{}' não disponível no momento. Ignorando na descoberta.", provider.getProviderId());
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
List<ManagedCertificate> certs = provider.listCertificates();
|
||||
logger.info("Provedor '{}' retornou {} certificado(s).", provider.getProviderId(), certs.size());
|
||||
|
||||
for (ManagedCertificate cert : certs) {
|
||||
certificateToProviderMap.put(cert.id(), provider);
|
||||
cachedCertificates.put(cert.id(), cert);
|
||||
discovered.add(cert);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro ao listar certificados do provedor '{}': {}", provider.getProviderId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenação: certificados disponíveis primeiro, depois por nome do titular
|
||||
discovered.sort(Comparator
|
||||
.comparing((ManagedCertificate c) -> c.status() == CertificateStatus.AVAILABLE ? 0 : 1)
|
||||
.thenComparing(ManagedCertificate::commonName, String.CASE_INSENSITIVE_ORDER));
|
||||
|
||||
logger.info("Descoberta concluída: {} certificado(s) total no agente.", discovered.size());
|
||||
return Collections.unmodifiableList(discovered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o provedor responsável pelo certificado com o ID informado.
|
||||
*/
|
||||
public CertificateProvider getProviderForCertificate(String certId) {
|
||||
if (certId == null) {
|
||||
return null;
|
||||
}
|
||||
return certificateToProviderMap.get(certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém o material criptográfico (chave privada e cadeia X.509) do certificado especificado.
|
||||
*/
|
||||
public Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certId, char[] pinOrPassword) throws CertificateProviderException {
|
||||
CertificateProvider provider = getProviderForCertificate(certId);
|
||||
if (provider == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return provider.getKeyMaterial(certId, pinOrPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca um certificado previamente descoberto pelo seu identificador único.
|
||||
*/
|
||||
public Optional<ManagedCertificate> findById(String certId) {
|
||||
if (certId == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(cachedCertificates.get(certId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Localiza um certificado usando hints flexíveis: id, alias, CPF, CNPJ ou impressões digitais SHA-1/SHA-256.
|
||||
*/
|
||||
public Optional<ManagedCertificate> findByHint(String hint) {
|
||||
if (hint == null || hint.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String cleanHint = hint.trim();
|
||||
|
||||
// 1. Busca por ID direto
|
||||
ManagedCertificate direct = cachedCertificates.get(cleanHint);
|
||||
if (direct != null) {
|
||||
return Optional.of(direct);
|
||||
}
|
||||
|
||||
// 2. Busca por alias, CPF, CNPJ ou fingerprints
|
||||
for (ManagedCertificate cert : cachedCertificates.values()) {
|
||||
if (cleanHint.equalsIgnoreCase(cert.alias())
|
||||
|| cleanHint.equalsIgnoreCase(cert.cpf())
|
||||
|| cleanHint.equalsIgnoreCase(cert.cnpj())
|
||||
|| cleanHint.equalsIgnoreCase(cert.sha1Fingerprint())
|
||||
|| cleanHint.equalsIgnoreCase(cert.sha256Fingerprint())
|
||||
|| cleanHint.equalsIgnoreCase(cert.serialNumber())) {
|
||||
return Optional.of(cert);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a lista dos certificados em cache que possuem status AVAILABLE (com chave privada e vigentes).
|
||||
*/
|
||||
public List<ManagedCertificate> getAvailableCertificates() {
|
||||
return cachedCertificates.values().stream()
|
||||
.filter(c -> c.status() == CertificateStatus.AVAILABLE)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Solicita recarregamento de todos os provedores cadastrados.
|
||||
*/
|
||||
public void refreshAll() {
|
||||
for (CertificateProvider provider : providers) {
|
||||
try {
|
||||
provider.refresh();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Erro ao recarregar provedor '{}': {}", provider.getProviderId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
discoverCertificates();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Contrato base para provedores de descoberta e assinatura com certificados digitais.
|
||||
* Permite plugar diferentes backends (A1 em arquivo, Windows-MY, PKCS#11, Mock)
|
||||
* sem acoplar o núcleo do aplicativo a fabricantes ou dispositivos específicos.
|
||||
*/
|
||||
public interface CertificateProvider {
|
||||
|
||||
/**
|
||||
* Identificador único do provedor (ex: "provider-a1", "provider-win-store", "provider-pkcs11").
|
||||
*/
|
||||
String getProviderId();
|
||||
|
||||
/**
|
||||
* Nome legível para apresentação na interface e diagnósticos.
|
||||
*/
|
||||
String getDisplayName();
|
||||
|
||||
/**
|
||||
* Tipo do provedor.
|
||||
*/
|
||||
ProviderType getType();
|
||||
|
||||
/**
|
||||
* Indica se o provedor está disponível e operacional no sistema atual.
|
||||
*/
|
||||
boolean isAvailable();
|
||||
|
||||
/**
|
||||
* Lista os certificados disponíveis atualmente através deste provedor.
|
||||
*/
|
||||
List<ManagedCertificate> listCertificates() throws CertificateProviderException;
|
||||
|
||||
/**
|
||||
* Executa a assinatura do digest contido no contexto.
|
||||
* A chave privada NUNCA deve sair do provedor/dispositivo.
|
||||
*/
|
||||
SigningResult sign(SigningContext context) throws CertificateProviderException;
|
||||
|
||||
/**
|
||||
* Retorna o material criptográfico (chave privada e cadeia) do certificado indicado,
|
||||
* permitindo que operações que montam envelopes como PAdES ou XMLDSig operem localmente.
|
||||
*/
|
||||
default java.util.Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Solicita recarga ou reescaneamento de certificados e dispositivos.
|
||||
*/
|
||||
default void refresh() {
|
||||
// Implementação padrão opcional
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
|
||||
/**
|
||||
* Exceção disparada por falhas nos provedores de certificado.
|
||||
* Carrega obrigatoriamente um código de erro padronizado do CCSP.
|
||||
*/
|
||||
public class CertificateProviderException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final CcspErrorCode errorCode;
|
||||
|
||||
public CertificateProviderException(CcspErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode != null ? errorCode : CcspErrorCode.SIGNATURE_FAILED;
|
||||
}
|
||||
|
||||
public CertificateProviderException(CcspErrorCode errorCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorCode = errorCode != null ? errorCode : CcspErrorCode.SIGNATURE_FAILED;
|
||||
}
|
||||
|
||||
public CertificateProviderException(CcspErrorCode errorCode) {
|
||||
this(errorCode, errorCode != null ? errorCode.getDefaultMessage() : CcspErrorCode.SIGNATURE_FAILED.getDefaultMessage());
|
||||
}
|
||||
|
||||
public CcspErrorCode getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
|
||||
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.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Provedor de certificados A3 via interface padrão PKCS#11 (tokens USB, smart cards e leitores).
|
||||
* Utiliza o provider SunPKCS11 integrado à JVM para comunicação com middlewares dos fabricantes.
|
||||
* Nunca persiste PIN em disco e nunca o transmite para servidores web.
|
||||
*/
|
||||
public class Pkcs11CertificateProvider implements CertificateProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Pkcs11CertificateProvider.class);
|
||||
private static final String PROVIDER_ID = "provider-pkcs11";
|
||||
|
||||
/**
|
||||
* Bibliotecas PKCS#11 comuns de middleware para tokens e smartcards no Windows e Linux.
|
||||
*/
|
||||
private static final String[] WELL_KNOWN_LIBRARIES = {
|
||||
// Windows
|
||||
"C:/Windows/System32/eps2003csp11.dll", // SafeNet / eToken
|
||||
"C:/Windows/System32/aetpkss1.dll", // SafeSign AET
|
||||
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
|
||||
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
|
||||
// Linux
|
||||
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
|
||||
"/usr/lib/opensc-pkcs11.so",
|
||||
"/usr/lib/libaetpkss.so"
|
||||
};
|
||||
|
||||
private Provider sunPkcs11Provider;
|
||||
private KeyStore keyStore;
|
||||
private String detectedLibraryPath;
|
||||
|
||||
public Pkcs11CertificateProvider() {
|
||||
detectAndInitialize();
|
||||
}
|
||||
|
||||
private synchronized void detectAndInitialize() {
|
||||
for (String libPath : WELL_KNOWN_LIBRARIES) {
|
||||
File f = new File(libPath);
|
||||
if (f.exists() && f.canRead()) {
|
||||
detectedLibraryPath = libPath;
|
||||
logger.info("Middleware PKCS#11 detectado no sistema: {}", libPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permite registrar explicitamente um provider PKCS#11 já inicializado (ex: via SunPKCS11 com PIN do usuário).
|
||||
*/
|
||||
public synchronized void setProvider(Provider provider, KeyStore keyStore) {
|
||||
this.sunPkcs11Provider = provider;
|
||||
this.keyStore = keyStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Hardware PKCS#11 (Smart Card / Token USB)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderType getType() {
|
||||
return ProviderType.A3_PKCS11;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isAvailable() {
|
||||
return (detectedLibraryPath != null) || (keyStore != null);
|
||||
}
|
||||
|
||||
public synchronized String getDetectedLibraryPath() {
|
||||
return detectedLibraryPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<ManagedCertificate> listCertificates() throws CertificateProviderException {
|
||||
if (keyStore == null) {
|
||||
// Se o middleware estiver detectado mas o token ainda não estiver autenticado via PIN
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
List<ManagedCertificate> certificates = new ArrayList<>();
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
boolean isKeyEntry = keyStore.isKeyEntry(alias);
|
||||
Certificate cert = keyStore.getCertificate(alias);
|
||||
|
||||
if (!(cert instanceof X509Certificate x509)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String certId = "p11-" + Integer.toHexString(alias.hashCode());
|
||||
ManagedCertificate mc = X509CertificateUtils.toManagedCertificate(
|
||||
certId,
|
||||
alias,
|
||||
x509,
|
||||
CertificateType.A3,
|
||||
isKeyEntry,
|
||||
PROVIDER_ID,
|
||||
"Token USB / Smart Card (PKCS#11)"
|
||||
);
|
||||
certificates.add(mc);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(certificates);
|
||||
} catch (KeyStoreException e) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PKCS11_ERROR,
|
||||
"Falha ao ler certificados do dispositivo PKCS#11: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException {
|
||||
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
|
||||
|
||||
if (keyStore == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.TOKEN_NOT_FOUND,
|
||||
"Dispositivo PKCS#11 não conectado ou não autenticado com PIN."
|
||||
);
|
||||
}
|
||||
|
||||
DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm());
|
||||
alg.validateDigestLength(context.digest());
|
||||
|
||||
String targetAlias = resolveAlias(context.certificateHint());
|
||||
if (targetAlias == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Certificado não encontrado no dispositivo PKCS#11."
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null);
|
||||
if (privateKey == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
|
||||
"Chave privada inacessível no dispositivo PKCS#11."
|
||||
);
|
||||
}
|
||||
|
||||
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
Certificate singleCert = keyStore.getCertificate(targetAlias);
|
||||
if (singleCert != null) {
|
||||
chain = new Certificate[]{singleCert};
|
||||
} else {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Cadeia do certificado não encontrada no dispositivo PKCS#11."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Executando assinatura digital no hardware PKCS#11 [alg={}]", alg.getJcaName());
|
||||
|
||||
Signature signature = (sunPkcs11Provider != null)
|
||||
? Signature.getInstance(alg.getJcaName(), sunPkcs11Provider)
|
||||
: Signature.getInstance(alg.getJcaName());
|
||||
|
||||
signature.initSign(privateKey);
|
||||
signature.update(context.digest());
|
||||
byte[] signatureBytes = signature.sign();
|
||||
|
||||
String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded());
|
||||
List<String> chainBase64 = new ArrayList<>();
|
||||
for (Certificate c : chain) {
|
||||
chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded()));
|
||||
}
|
||||
|
||||
return new SigningResult(
|
||||
context.requestId(),
|
||||
signatureBytes,
|
||||
certBase64,
|
||||
chainBase64,
|
||||
alg.getJcaName(),
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
} catch (CertificateProviderException cpe) {
|
||||
throw cpe;
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro na assinatura via dispositivo PKCS#11: {}", e.getMessage(), e);
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PKCS11_ERROR,
|
||||
"Erro ao assinar com o dispositivo PKCS#11: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveAlias(String hint) {
|
||||
try {
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
List<String> list = Collections.list(aliases);
|
||||
if (hint == null || hint.isBlank()) {
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
for (String alias : list) {
|
||||
String certId = "p11-" + Integer.toHexString(alias.hashCode());
|
||||
if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
|
||||
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.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Provedor de certificados A1 em formato PKCS#12 (.pfx / .p12).
|
||||
* Mantém chaves e senhas isoladas na memória local do agente sem persistência insegura ou vazamento em logs.
|
||||
*/
|
||||
public class Pkcs12CertificateProvider implements CertificateProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Pkcs12CertificateProvider.class);
|
||||
private static final String PROVIDER_ID = "provider-a1";
|
||||
|
||||
private final String providerId;
|
||||
private final String displayName;
|
||||
private KeyStore keyStore;
|
||||
private char[] currentPassword;
|
||||
|
||||
public Pkcs12CertificateProvider() {
|
||||
this(PROVIDER_ID, "Certificados A1 (PKCS#12)");
|
||||
}
|
||||
|
||||
public Pkcs12CertificateProvider(String providerId, String displayName) {
|
||||
this.providerId = Objects.requireNonNull(providerId);
|
||||
this.displayName = Objects.requireNonNull(displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega um KeyStore PKCS#12 diretamente a partir de bytes.
|
||||
* A senha é utilizada exclusivamente para decifrar a chave em memória e nunca é gravada em logs.
|
||||
*/
|
||||
public synchronized void loadFromBytes(byte[] pfxData, char[] password) throws CertificateProviderException {
|
||||
Objects.requireNonNull(pfxData, "Dados do certificado não podem ser nulos.");
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(pfxData)) {
|
||||
loadFromStream(bais, password);
|
||||
} catch (CertificateProviderException cpe) {
|
||||
throw cpe;
|
||||
} catch (Exception e) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_INVALID,
|
||||
"Falha ao processar arquivo PKCS#12: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega um KeyStore PKCS#12 a partir de um InputStream.
|
||||
*/
|
||||
public synchronized void loadFromStream(InputStream stream, char[] password) throws CertificateProviderException {
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(stream, password);
|
||||
this.keyStore = ks;
|
||||
this.currentPassword = password != null ? password.clone() : new char[0];
|
||||
logger.info("KeyStore PKCS#12 carregado com sucesso no provedor '{}'", providerId);
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : "";
|
||||
if (msg.contains("password") || msg.contains("keystore password was incorrect") || msg.contains("mac")) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PIN_INCORRECT,
|
||||
"Senha do certificado A1 incorreta.",
|
||||
e
|
||||
);
|
||||
}
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_INVALID,
|
||||
"Não foi possível carregar o arquivo PKCS#12: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define diretamente uma instância de KeyStore PKCS#12 pré-carregada (útil em testes ou integrações).
|
||||
*/
|
||||
public synchronized void setKeyStore(KeyStore keyStore, char[] password) {
|
||||
this.keyStore = keyStore;
|
||||
this.currentPassword = password != null ? password.clone() : new char[0];
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
this.keyStore = null;
|
||||
if (this.currentPassword != null) {
|
||||
java.util.Arrays.fill(this.currentPassword, ' ');
|
||||
this.currentPassword = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderType getType() {
|
||||
return ProviderType.A1_FILE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean isAvailable() {
|
||||
return keyStore != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<ManagedCertificate> listCertificates() throws CertificateProviderException {
|
||||
if (!isAvailable()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
List<ManagedCertificate> certificates = new ArrayList<>();
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
boolean isKeyEntry = keyStore.isKeyEntry(alias);
|
||||
Certificate cert = keyStore.getCertificate(alias);
|
||||
|
||||
if (!(cert instanceof X509Certificate x509)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String certId = "a1-" + Integer.toHexString(alias.hashCode());
|
||||
ManagedCertificate mc = X509CertificateUtils.toManagedCertificate(
|
||||
certId,
|
||||
alias,
|
||||
x509,
|
||||
CertificateType.A1,
|
||||
isKeyEntry,
|
||||
providerId,
|
||||
"Arquivo PKCS#12 (.pfx/.p12)"
|
||||
);
|
||||
certificates.add(mc);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(certificates);
|
||||
} catch (KeyStoreException e) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
|
||||
"Erro ao consultar certificados do KeyStore PKCS#12: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException {
|
||||
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
|
||||
|
||||
if (!isAvailable()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
|
||||
"Nenhum certificado A1 carregado neste provedor."
|
||||
);
|
||||
}
|
||||
|
||||
DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm());
|
||||
alg.validateDigestLength(context.digest());
|
||||
|
||||
String targetAlias = resolveAlias(context.certificateHint());
|
||||
if (targetAlias == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Certificado A1 não encontrado para a requisição: " + context.certificateHint()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, currentPassword);
|
||||
if (privateKey == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
|
||||
"Chave privada não encontrada para o certificado: " + targetAlias
|
||||
);
|
||||
}
|
||||
|
||||
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
Certificate singleCert = keyStore.getCertificate(targetAlias);
|
||||
if (singleCert != null) {
|
||||
chain = new Certificate[]{singleCert};
|
||||
} else {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Cadeia de certificados ausente para o alias: " + targetAlias
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Executando assinatura com certificado A1 [alg={}]", alg.getJcaName());
|
||||
|
||||
Signature signature = Signature.getInstance(alg.getJcaName());
|
||||
signature.initSign(privateKey);
|
||||
signature.update(context.digest());
|
||||
byte[] signatureBytes = signature.sign();
|
||||
|
||||
String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded());
|
||||
List<String> chainBase64 = new ArrayList<>();
|
||||
for (Certificate c : chain) {
|
||||
chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded()));
|
||||
}
|
||||
|
||||
return new SigningResult(
|
||||
context.requestId(),
|
||||
signatureBytes,
|
||||
certBase64,
|
||||
chainBase64,
|
||||
alg.getJcaName(),
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
} catch (CertificateProviderException cpe) {
|
||||
throw cpe;
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro ao realizar assinatura com certificado A1: {}", e.getMessage(), e);
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Erro ao assinar com certificado A1: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized java.util.Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException {
|
||||
if (!isAvailable()) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
String targetAlias = resolveAlias(certificateId);
|
||||
if (targetAlias == null) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
char[] passToUse = (currentPassword != null && currentPassword.length > 0) ? currentPassword : pinOrPassword;
|
||||
try {
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, passToUse);
|
||||
if (privateKey == null) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
Certificate single = keyStore.getCertificate(targetAlias);
|
||||
if (single != null) {
|
||||
chain = new Certificate[]{single};
|
||||
}
|
||||
}
|
||||
if (chain == null || chain.length == 0) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
X509Certificate[] x509Chain = new X509Certificate[chain.length];
|
||||
for (int i = 0; i < chain.length; i++) {
|
||||
x509Chain[i] = (X509Certificate) chain[i];
|
||||
}
|
||||
return java.util.Optional.of(new br.com.cybercore.signer.core.domain.SigningKeyMaterial(privateKey, x509Chain, null));
|
||||
} catch (Exception e) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Falha ao obter chave privada e certificados A1: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveAlias(String hint) {
|
||||
try {
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
List<String> list = Collections.list(aliases);
|
||||
if (hint == null || hint.isBlank()) {
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
for (String alias : list) {
|
||||
String certId = "a1-" + Integer.toHexString(alias.hashCode());
|
||||
if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) {
|
||||
return alias;
|
||||
}
|
||||
Certificate c = keyStore.getCertificate(alias);
|
||||
if (c instanceof X509Certificate x509) {
|
||||
String sha1 = X509CertificateUtils.calculateFingerprint(x509, "SHA-1");
|
||||
String sha256 = X509CertificateUtils.calculateFingerprint(x509, "SHA-256");
|
||||
if (sha1.equalsIgnoreCase(hint) || sha256.equalsIgnoreCase(hint)) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
/**
|
||||
* Classificação da tecnologia ou repositório do provedor de certificados.
|
||||
*/
|
||||
public enum ProviderType {
|
||||
A1_FILE("Arquivo PKCS#12 (.pfx/.p12)"),
|
||||
A3_WINDOWS_STORE("Windows Certificate Store (CAPI/CNG)"),
|
||||
A3_PKCS11("Hardware PKCS#11 (Smart Card / Token USB)"),
|
||||
MOCK("Simulador de Desenvolvimento");
|
||||
|
||||
private final String description;
|
||||
|
||||
ProviderType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package br.com.cybercore.signer.core.provider;
|
||||
|
||||
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
|
||||
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
|
||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.CertificateType;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Provedor de certificados integrado nativamente ao repositório de certificados do Windows (Windows-MY).
|
||||
* Utiliza o subsistema SunMSCAPI/CAPI/CNG para gerenciar chaves privadas e certificados A1/A3 sem expor credenciais.
|
||||
*/
|
||||
public class WindowsCertificateStoreProvider implements CertificateProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WindowsCertificateStoreProvider.class);
|
||||
private static final String PROVIDER_ID = "provider-win-store";
|
||||
private static final String KEYSTORE_TYPE = "Windows-MY";
|
||||
|
||||
private KeyStore keyStore;
|
||||
private final boolean isWindows;
|
||||
|
||||
public WindowsCertificateStoreProvider() {
|
||||
String osName = System.getProperty("os.name", "").toLowerCase();
|
||||
this.isWindows = osName.contains("win");
|
||||
if (this.isWindows) {
|
||||
initKeyStore();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void initKeyStore() {
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
|
||||
this.keyStore.load(null, null);
|
||||
logger.info("Windows Certificate Store (Windows-MY) inicializado com sucesso.");
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Não foi possível carregar o repositório Windows-MY: {}", t.getMessage());
|
||||
this.keyStore = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Windows Certificate Store (Windows-MY)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderType getType() {
|
||||
return ProviderType.A3_WINDOWS_STORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return isWindows && keyStore != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized List<ManagedCertificate> listCertificates() throws CertificateProviderException {
|
||||
if (!isAvailable()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
// Recarrega o repositório para detectar tokens recém-inseridos
|
||||
this.keyStore.load(null, null);
|
||||
|
||||
List<ManagedCertificate> certificates = new ArrayList<>();
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
try {
|
||||
if (!keyStore.isKeyEntry(alias)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Certificate cert = keyStore.getCertificate(alias);
|
||||
if (!(cert instanceof X509Certificate x509)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Heurística de tipo A1 vs A3:
|
||||
// ICP-Brasil A3 tem tipicamente 3 anos (validade > ~400 dias).
|
||||
// A1 tem validade de até 1 ano (<= 370 dias).
|
||||
long validityDays = ChronoUnit.DAYS.between(x509.getNotBefore().toInstant(), x509.getNotAfter().toInstant());
|
||||
CertificateType certType = validityDays > 400 ? CertificateType.A3 : CertificateType.A1;
|
||||
String deviceName = certType == CertificateType.A3
|
||||
? "Token / Smart Card (Windows CAPI/CNG)"
|
||||
: "Repositório do Windows (Software A1)";
|
||||
|
||||
String certId = "win-" + Integer.toHexString(alias.hashCode());
|
||||
|
||||
ManagedCertificate managedCert = X509CertificateUtils.toManagedCertificate(
|
||||
certId,
|
||||
alias,
|
||||
x509,
|
||||
certType,
|
||||
true,
|
||||
PROVIDER_ID,
|
||||
deviceName
|
||||
);
|
||||
|
||||
certificates.add(managedCert);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Ignorando alias '{}' devido a erro na leitura: {}", alias, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(certificates);
|
||||
} catch (Exception e) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
|
||||
"Falha ao enumerar certificados do Windows Certificate Store: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException {
|
||||
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
|
||||
|
||||
if (!isAvailable()) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
|
||||
"Repositório Windows Certificate Store não está disponível neste sistema operacional."
|
||||
);
|
||||
}
|
||||
|
||||
// Valida algoritmo e tamanho de digest
|
||||
DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm());
|
||||
alg.validateDigestLength(context.digest());
|
||||
|
||||
String targetAlias = resolveAlias(context.certificateHint());
|
||||
if (targetAlias == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Nenhum certificado correspondente encontrado no Windows Certificate Store para o hint: " + context.certificateHint()
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null);
|
||||
if (privateKey == null) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
|
||||
"Chave privada não acessível para o certificado: " + targetAlias
|
||||
);
|
||||
}
|
||||
|
||||
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
Certificate singleCert = keyStore.getCertificate(targetAlias);
|
||||
if (singleCert != null) {
|
||||
chain = new Certificate[]{singleCert};
|
||||
} else {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||
"Cadeia de certificação não encontrada para o alias: " + targetAlias
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Executando assinatura digital com algoritmo {} via Windows-MY para alias '{}'",
|
||||
alg.getJcaName(), targetAlias);
|
||||
|
||||
Signature signature = Signature.getInstance(alg.getJcaName());
|
||||
signature.initSign(privateKey);
|
||||
signature.update(context.digest());
|
||||
byte[] signatureBytes = signature.sign();
|
||||
|
||||
String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded());
|
||||
List<String> chainBase64 = new ArrayList<>();
|
||||
for (Certificate c : chain) {
|
||||
chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded()));
|
||||
}
|
||||
|
||||
return new SigningResult(
|
||||
context.requestId(),
|
||||
signatureBytes,
|
||||
certBase64,
|
||||
chainBase64,
|
||||
alg.getJcaName(),
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
} catch (CertificateProviderException cpe) {
|
||||
throw cpe;
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro durante a assinatura via Windows-MY: {}", e.getMessage(), e);
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Erro ao assinar com o Windows Certificate Store: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
// Força liberação de handles no SunMSCAPI para que o driver de SmartCard/Token
|
||||
// possa reavaliar o estado de autenticação de PIN
|
||||
if (keyStore != null) {
|
||||
keyStore.load(null, null);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Se configurado para sempre exigir PIN (sem cache), forçamos o reset da sessão no Smart Card
|
||||
// para que o Windows BaseCSP e o SafeSign invalidem o cache de processo e peçam o PIN na próxima assinatura
|
||||
if (br.com.cybercore.signer.core.security.PinSessionManager.getInstance().isAlwaysPromptPin()) {
|
||||
br.com.cybercore.signer.core.security.SmartCardSessionManager.resetCardSessions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveAlias(String hint) {
|
||||
if (hint == null || hint.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
String certId = "win-" + Integer.toHexString(alias.hashCode());
|
||||
if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) {
|
||||
return alias;
|
||||
}
|
||||
Certificate c = keyStore.getCertificate(alias);
|
||||
if (c instanceof X509Certificate x509) {
|
||||
String sha1 = X509CertificateUtils.calculateFingerprint(x509, "SHA-1");
|
||||
String sha256 = X509CertificateUtils.calculateFingerprint(x509, "SHA-256");
|
||||
if (sha1.equalsIgnoreCase(hint) || sha256.equalsIgnoreCase(hint)) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized java.util.Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException {
|
||||
if (!isAvailable()) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
String targetAlias = resolveAlias(certificateId);
|
||||
if (targetAlias == null) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
try {
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null);
|
||||
if (privateKey == null) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
|
||||
if (chain == null || chain.length == 0) {
|
||||
Certificate single = keyStore.getCertificate(targetAlias);
|
||||
if (single != null) {
|
||||
chain = new Certificate[]{single};
|
||||
}
|
||||
}
|
||||
if (chain == null || chain.length == 0) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
X509Certificate[] x509Chain = new X509Certificate[chain.length];
|
||||
for (int i = 0; i < chain.length; i++) {
|
||||
x509Chain[i] = (X509Certificate) chain[i];
|
||||
}
|
||||
return java.util.Optional.of(new br.com.cybercore.signer.core.domain.SigningKeyMaterial(privateKey, x509Chain, keyStore.getProvider()));
|
||||
} catch (Exception e) {
|
||||
throw new CertificateProviderException(
|
||||
CcspErrorCode.SIGNATURE_FAILED,
|
||||
"Falha ao obter chave privada e certificados no Windows Store: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void refresh() {
|
||||
if (isWindows) {
|
||||
initKeyStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package br.com.cybercore.signer.core.security;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Registro de aplicações web autorizadas a se comunicar com o agente CyberCore Signer.
|
||||
* Suporta persistência segura no diretório do usuário (~/.cybercore/authorized-apps.json).
|
||||
*/
|
||||
public final class ApplicationRegistry {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApplicationRegistry.class);
|
||||
private static final String DEFAULT_FILE_NAME = "authorized-apps.json";
|
||||
|
||||
private static final ApplicationRegistry INSTANCE = new ApplicationRegistry();
|
||||
|
||||
private final Path storagePath;
|
||||
private final Map<String, AuthorizedApplication> applications = new ConcurrentHashMap<>();
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public static ApplicationRegistry getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public ApplicationRegistry() {
|
||||
this(resolveDefaultStoragePath());
|
||||
}
|
||||
|
||||
public ApplicationRegistry(Path storagePath) {
|
||||
this.storagePath = storagePath;
|
||||
this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
load();
|
||||
}
|
||||
|
||||
private static Path resolveDefaultStoragePath() {
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
return Path.of(userHome, ".cybercore", DEFAULT_FILE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca uma aplicação pelo seu ID único.
|
||||
*/
|
||||
public Optional<AuthorizedApplication> findById(String applicationId) {
|
||||
if (applicationId == null || applicationId.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(applications.get(applicationId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca uma aplicação que contenha a origem especificada autorizada.
|
||||
*/
|
||||
public Optional<AuthorizedApplication> findByOrigin(String origin) {
|
||||
if (origin == null || origin.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return applications.values().stream()
|
||||
.filter(app -> app.isOriginAllowed(origin))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma aplicação e sua respectiva origem estão autorizadas e ativas.
|
||||
*/
|
||||
public boolean isAuthorized(String applicationId, String origin) {
|
||||
Optional<AuthorizedApplication> appOpt = findById(applicationId);
|
||||
return appOpt.map(app -> app.isOriginAllowed(origin)).orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra ou atualiza uma aplicação autorizada e persiste a alteração.
|
||||
*/
|
||||
public synchronized void register(AuthorizedApplication app) {
|
||||
Objects.requireNonNull(app, "Aplicação não pode ser nula");
|
||||
applications.put(app.applicationId(), app);
|
||||
logger.info("Aplicação registrada: '{}' [id={}] com origens: {}", app.name(), app.applicationId(), app.allowedOrigins());
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoga a autorização de uma aplicação.
|
||||
*/
|
||||
public synchronized void revoke(String applicationId) {
|
||||
if (applicationId != null && applications.remove(applicationId) != null) {
|
||||
logger.info("Autorização revogada para a aplicação [id={}]", applicationId);
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a lista imutável de todas as aplicações cadastradas.
|
||||
*/
|
||||
public List<AuthorizedApplication> listAll() {
|
||||
return Collections.unmodifiableList(new ArrayList<>(applications.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega as aplicações persistidas em disco.
|
||||
*/
|
||||
public synchronized void load() {
|
||||
if (storagePath == null || !Files.exists(storagePath)) {
|
||||
logger.debug("Nenhum arquivo de aplicações persistidas encontrado em {}", storagePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<AuthorizedApplication> list = objectMapper.readValue(
|
||||
storagePath.toFile(),
|
||||
new TypeReference<List<AuthorizedApplication>>() {}
|
||||
);
|
||||
applications.clear();
|
||||
for (AuthorizedApplication app : list) {
|
||||
applications.put(app.applicationId(), app);
|
||||
}
|
||||
logger.info("Carregadas {} aplicação(ões) autorizada(s) de {}", applications.size(), storagePath);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao ler aplicações autorizadas de {}: {}", storagePath, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva as aplicações atuais em disco.
|
||||
*/
|
||||
public synchronized void save() {
|
||||
if (storagePath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Path parent = storagePath.getParent();
|
||||
if (parent != null && !Files.exists(parent)) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
storagePath.toFile(),
|
||||
new ArrayList<>(applications.values())
|
||||
);
|
||||
logger.debug("Aplicações autorizadas salvas com sucesso em {}", storagePath);
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro ao salvar aplicações autorizadas em {}: {}", storagePath, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
applications.clear();
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package br.com.cybercore.signer.core.security;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Representa uma aplicação web autorizada a solicitar assinaturas ao CyberCore Signer.
|
||||
*/
|
||||
public record AuthorizedApplication(
|
||||
String applicationId,
|
||||
String name,
|
||||
Set<String> allowedOrigins,
|
||||
boolean enabled,
|
||||
Instant registeredAt
|
||||
) implements Serializable {
|
||||
|
||||
public AuthorizedApplication {
|
||||
Objects.requireNonNull(applicationId, "applicationId must not be null");
|
||||
Objects.requireNonNull(name, "name must not be null");
|
||||
allowedOrigins = allowedOrigins != null ? Set.copyOf(allowedOrigins) : Set.of();
|
||||
if (registeredAt == null) {
|
||||
registeredAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma determinada Origin web está autorizada para esta aplicação.
|
||||
*/
|
||||
public boolean isOriginAllowed(String origin) {
|
||||
if (!enabled || origin == null || origin.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = origin.trim().toLowerCase();
|
||||
for (String allowed : allowedOrigins) {
|
||||
if (allowed.trim().toLowerCase().equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package br.com.cybercore.signer.core.security;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Gerenciador de nonces em memória para proteção contra ataques de replay (CCSP/1).
|
||||
* Garante que uma requisição com o mesmo nonce e correlation_id não seja reprocessada.
|
||||
*/
|
||||
public class NonceManager {
|
||||
|
||||
private final Duration windowDuration;
|
||||
private final ConcurrentHashMap<String, Instant> usedNonces = new ConcurrentHashMap<>();
|
||||
|
||||
public NonceManager(Duration windowDuration) {
|
||||
this.windowDuration = windowDuration != null ? windowDuration : Duration.ofSeconds(120);
|
||||
}
|
||||
|
||||
public NonceManager() {
|
||||
this(Duration.ofSeconds(120));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenta registrar um nonce.
|
||||
*
|
||||
* @param nonce Nonce gerado pelo cliente/requisição.
|
||||
* @return {@code true} se o nonce for novo e válido; {@code false} se já tiver sido utilizado (replay).
|
||||
*/
|
||||
public boolean registerNonce(String nonce) {
|
||||
if (nonce == null || nonce.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cleanupExpired();
|
||||
|
||||
Instant now = Instant.now();
|
||||
Instant existing = usedNonces.putIfAbsent(nonce, now);
|
||||
return existing == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove nonces expirados além da janela máxima de aceitação.
|
||||
*/
|
||||
public void cleanupExpired() {
|
||||
Instant cutoff = Instant.now().minus(windowDuration);
|
||||
usedNonces.entrySet().removeIf(entry -> entry.getValue().isBefore(cutoff));
|
||||
}
|
||||
|
||||
public int activeNonceCount() {
|
||||
return usedNonces.size();
|
||||
}
|
||||
}
|
||||