Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1d4f359e | ||
|
|
8d0e587114 | ||
|
|
b19d09a236 | ||
|
|
50b5feced5 | ||
|
|
9dcbac61ec | ||
|
|
957f9c9abd | ||
|
|
e9f90b42cb | ||
|
|
92d4ea3411 | ||
|
|
49021d9942 | ||
|
|
67e47cbed5 | ||
|
|
b00f78505d | ||
|
|
a32ab3b7ce | ||
|
|
e3d56d90af | ||
|
|
051614a919 | ||
|
|
bac691aae1 | ||
|
|
71428cdaad | ||
|
|
cf7ac8c51c | ||
|
|
002823ede3 | ||
|
|
90e2c8ef3d | ||
|
|
4b6ae923c6 | ||
|
|
4b64a113b5 | ||
|
|
06c90b4e86 | ||
|
|
300944697f | ||
|
|
7aa33a4789 | ||
|
|
a562909bc5 | ||
|
|
66459bfae4 | ||
|
|
1a727e6ff5 | ||
|
|
eeeecb0604 | ||
|
|
4e6dd2a029 | ||
|
|
cd3ef646d2 | ||
|
|
fa93e31ca1 | ||
|
|
6093879aa9 | ||
|
|
cebeca467f | ||
|
|
e50257fb34 | ||
|
|
6dce9f9317 | ||
|
|
539d5e4c33 | ||
|
|
5551d9e667 | ||
|
|
ef44d34944 | ||
|
|
edccfff7ff | ||
|
|
16212702cb | ||
|
|
6f191c6a69 | ||
|
|
d1cfab95c2 | ||
|
|
64ca13b3a1 |
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: "Relato de Bug"
|
||||||
|
about: "Relate um problema ou comportamento inesperado no CyberCore Signer"
|
||||||
|
title: "[BUG] "
|
||||||
|
labels:
|
||||||
|
- "bug"
|
||||||
|
---
|
||||||
|
|
||||||
|
### Descrição do Problema
|
||||||
|
Uma descrição clara e concisa do comportamento inesperado ocorrido.
|
||||||
|
|
||||||
|
### Passos para Reproduzir
|
||||||
|
1. Iniciar o CyberCore Signer com o comando `...`
|
||||||
|
2. Acessar a aplicação web ou endpoint `...`
|
||||||
|
3. Tentar executar a operação de assinatura `...`
|
||||||
|
4. Observar o erro reportado.
|
||||||
|
|
||||||
|
### Comportamento Esperado
|
||||||
|
O que você esperava que acontecesse normalmente.
|
||||||
|
|
||||||
|
### Logs e Mensagens de Diagnóstico
|
||||||
|
```text
|
||||||
|
Cole aqui o trecho relevante de log do agente (sem expor dados sensíveis ou chaves).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Informações do Ambiente
|
||||||
|
* **Sistema Operacional:** Windows 10 / Windows 11 / Ubuntu / Outro (especifique)
|
||||||
|
* **Versão do Java:** 25 LTS (especifique a vendor: Temurin, Corretto, Oracle)
|
||||||
|
* **Tipo de Certificado Utilizado:** A1 (arquivo `.pfx`) / A3 (Token USB / Smart Card / Windows-MY)
|
||||||
|
* **Fabricante do Hardware A3 (se aplicável):** SafeSign / SafeNet / GD Burti / Outro
|
||||||
|
* **Navegador e Versão (se aplicável ao SDK):** Chrome / Edge / Firefox
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: "Solicitação de Funcionalidade"
|
||||||
|
about: "Sugira uma nova funcionalidade ou melhoria para o agente ou SDK"
|
||||||
|
title: "[FEAT] "
|
||||||
|
labels:
|
||||||
|
- "enhancement"
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problema ou Necessidade
|
||||||
|
Descreva o cenário de uso ou o desafio que motivou esta solicitação.
|
||||||
|
|
||||||
|
### Solução Proposta
|
||||||
|
Uma descrição clara do recurso ou melhoria que você gostaria de ver implementado no CyberCore Signer.
|
||||||
|
|
||||||
|
### Módulos Afetados
|
||||||
|
* [ ] `modules/cybercore-core` (regras criptográficas, provedores, protocolo CCSP)
|
||||||
|
* [ ] `modules/cybercore-ui` (telas JavaFX, diálogos, selo visual, system tray)
|
||||||
|
* [ ] `modules/cybercore-app` (ponto de entrada, empacotamento nativo)
|
||||||
|
* [ ] `sdk/` (SDK Web JavaScript)
|
||||||
|
* [ ] `docs/` (especificações e manuais)
|
||||||
|
|
||||||
|
### Alternativas Consideradas
|
||||||
|
Descreva quaisquer alternativas ou soluções de contorno que você considerou.
|
||||||
|
|
||||||
|
### Contexto Adicional
|
||||||
|
Adicione qualquer imagem, rascunho de tela ou especificação relevante.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
### Descrição das Alterações
|
||||||
|
Resuma de forma clara o objetivo deste Pull Request e as alterações realizadas.
|
||||||
|
|
||||||
|
### Tipo de Mudança
|
||||||
|
* [ ] Correção de bug (`fix`)
|
||||||
|
* [ ] Nova funcionalidade (`feat`)
|
||||||
|
* [ ] Refatoração sem alteração de comportamento (`refactor`)
|
||||||
|
* [ ] Atualização de documentação (`docs`)
|
||||||
|
* [ ] Ajuste de testes ou métricas de cobertura (`test`)
|
||||||
|
* [ ] Manutenção de infraestrutura ou CI (`ci` / `chore`)
|
||||||
|
|
||||||
|
### Checklist de Qualidade e Segurança
|
||||||
|
* [ ] O código segue o padrão e regras de arquitetura do projeto.
|
||||||
|
* [ ] Nenhuma credencial, PIN, senha ou chave privada real foi comitada.
|
||||||
|
* [ ] A validação `./mvnw clean verify` foi executada e passou com 100% de sucesso.
|
||||||
|
* [ ] A cobertura mínima do JaCoCo foi respeitada em todos os módulos afetados.
|
||||||
|
* [ ] As mensagens de commit seguem a convenção *Conventional Commits*.
|
||||||
|
* [ ] A documentação técnica foi atualizada (quando aplicável).
|
||||||
|
|
||||||
|
### Issue Relacionada
|
||||||
|
Fecha #
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
name: CI Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'feature/**'
|
||||||
|
- 'fix/**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
name: Build & Verify (Linux x86_64)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repositório
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configurar Java 25 LTS (Temurin)
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '25'
|
||||||
|
cache: 'maven'
|
||||||
|
|
||||||
|
- name: Conceder permissão de execução ao Maven Wrapper
|
||||||
|
run: chmod +x ./mvnw
|
||||||
|
|
||||||
|
- name: Instalar SoftHSM2 para testes de hardware PKCS#11
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y softhsm2
|
||||||
|
sudo mkdir -p /var/lib/softhsm/tokens
|
||||||
|
sudo chmod 777 /var/lib/softhsm/tokens
|
||||||
|
|
||||||
|
- name: Executar verificação Maven (Compilação, Testes, JaCoCo, SpotBugs, ArchUnit)
|
||||||
|
run: ./mvnw -B clean verify
|
||||||
|
|
||||||
|
- name: Publicar Relatórios de Cobertura JaCoCo
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: jacoco-reports-linux
|
||||||
|
path: |
|
||||||
|
modules/*/target/site/jacoco/**
|
||||||
|
modules/*/target/jacoco.exec
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Publicar Relatórios de Análise Estática SpotBugs
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: spotbugs-reports-linux
|
||||||
|
path: |
|
||||||
|
modules/*/target/spotbugsXml.xml
|
||||||
|
modules/*/target/site/spotbugs/**
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Publicar CycloneDX SBOM (Software Bill of Materials)
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: cyclonedx-sbom
|
||||||
|
path: |
|
||||||
|
target/bom.json
|
||||||
|
target/bom.xml
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
name: Build & Verify (Windows)
|
||||||
|
# Runner Windows: requer act_runner nativo em Windows na instância Gitea.
|
||||||
|
# Caso sua instância utilize apenas Docker Linux, configure labels: [windows] no host correspondente.
|
||||||
|
runs-on: windows-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout repositório
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configurar Java 25 LTS (Temurin)
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '25'
|
||||||
|
cache: 'maven'
|
||||||
|
|
||||||
|
- name: Executar verificação Maven no Windows
|
||||||
|
run: .\mvnw.cmd -B clean verify
|
||||||
|
shell: cmd
|
||||||
|
|
||||||
|
- name: Publicar Relatórios JaCoCo (Windows)
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: jacoco-reports-windows
|
||||||
|
path: |
|
||||||
|
modules/*/target/site/jacoco/**
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Publicar Relatórios SpotBugs (Windows)
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: spotbugs-reports-windows
|
||||||
|
path: |
|
||||||
|
modules/*/target/spotbugsXml.xml
|
||||||
|
retention-days: 14
|
||||||
@@ -77,4 +77,8 @@ $RECYCLE.BIN/
|
|||||||
*~
|
*~
|
||||||
|
|
||||||
# Arquivos temporários de Scratch locais
|
# Arquivos temporários de Scratch locais
|
||||||
scratch/temp/
|
scratch/
|
||||||
|
|
||||||
|
# Agentes e caches locais
|
||||||
|
.agents/**/cache/
|
||||||
|
.agents/**/temp/
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Todas as mudanças relevantes deste projeto serão documentadas neste arquivo.
|
||||||
|
|
||||||
|
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
||||||
|
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Não publicado]
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Pipeline de integração contínua (CI) via Gitea Actions para Linux e Windows.
|
||||||
|
- Verificação de cobertura mínima de testes via meta `jacoco:check` calibrada por módulo.
|
||||||
|
- Configuração de atualização automatizada de dependências via Renovate com agrupamento específico.
|
||||||
|
- Templates de issues e pull requests para Gitea em `.gitea/`.
|
||||||
|
- Guia de contribuição `CONTRIBUTING.md` e especificação de release em `docs/release-and-versioning.md`.
|
||||||
|
|
||||||
|
### Modificado
|
||||||
|
- Alinhamento da documentação e scripts de automação para exigir estritamente o JDK 25 LTS.
|
||||||
|
- Refinamento do `.gitignore` para ignorar a pasta `scratch/` e mover script utilitário para `scripts/`.
|
||||||
|
- Atualização do filtro de exclusão do SpotBugs com justificativas técnicas detalhadas para garantir verificação completa e limpa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-10-08
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- **Núcleo Criptográfico (`cybercore-core`):**
|
||||||
|
- Gerenciador central de certificados digitais (`CertificateManager`).
|
||||||
|
- Provedor para certificados em arquivo A1 PKCS#12 (`.pfx` / `.p12`).
|
||||||
|
- Provedor para repositório do Windows (`Windows-MY` via SunMSCAPI / CNG).
|
||||||
|
- Provedor para dispositivos criptográficos A3 PKCS#11 (tokens USB e smartcards).
|
||||||
|
- Assinatura digital padrão PAdES (PDF) em conformidade com as normas ICP-Brasil (Apache PDFBox e Bouncy Castle).
|
||||||
|
- Assinatura digital padrão XAdES-BES / XMLDSig para notas e documentos fiscais eletrônicos.
|
||||||
|
- Suporte a carimbo de tempo TSA e validação de longo prazo (LTV).
|
||||||
|
- Servidor HTTP local CCSP/1 (`127.0.0.1:21325`) com bind exclusivo em loopback, proteção anti-replay via nonce e controle de concorrência por fila.
|
||||||
|
- **Interface Gráfica Nativa (`cybercore-ui`):**
|
||||||
|
- Aplicação moderna em JavaFX 25 com suporte a tema Claro e Escuro (Dual-Theme).
|
||||||
|
- Dashboard de status do agente e leitor de certificados em tempo real.
|
||||||
|
- Diálogos de autorização prévia de aplicações web e confirmação visual de assinaturas.
|
||||||
|
- Designer e visualizador de selos visuais em PDF com renderização SVG e Java2D.
|
||||||
|
- Integração com a bandeja do sistema operacional (System Tray).
|
||||||
|
- **Módulo Executável (`cybercore-app`):**
|
||||||
|
- Ponto de entrada consolidado (`CyberCoreSignerApplication`), rotinas de telemetria diagnóstica e testes de arquitetura com ArchUnit.
|
||||||
|
- Empacotamento de Fat JAR autocontido via Maven Shade Plugin.
|
||||||
|
- **SDK JavaScript (`@cybercore/signer-sdk`):**
|
||||||
|
- Cliente web com métodos `connect()`, `isInstalled()`, `getCertificates()`, `sign()`, `verify()`.
|
||||||
|
- Página de demonstração interativa para testes de integração web.
|
||||||
|
- **Documentação Técnica:**
|
||||||
|
- Especificação do protocolo CCSP/1 (`docs/CCSP.md`).
|
||||||
|
- Diretrizes de segurança, arquitetura, diagnóstico e manuais de instalação.
|
||||||
|
|
||||||
|
### Segurança
|
||||||
|
- Isolamento estrito de chaves privadas e senhas na máquina do usuário local (nunca trafegadas pela rede nem registradas em logs).
|
||||||
|
- Destruição segura de senhas em memória após uso via `Arrays.fill(..., '\0')`.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Guia de Contribuição — CyberCore Signer
|
||||||
|
|
||||||
|
Agradecemos o interesse em contribuir com o **CyberCore Signer**! Para garantir a segurança criptográfica, a estabilidade e a manutenibilidade do projeto, siga as orientações descritas abaixo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Pré-requisitos do Ambiente
|
||||||
|
|
||||||
|
* **Java Development Kit (JDK):** Versão 25 LTS (Eclipse Temurin, OpenJDK ou Amazon Corretto).
|
||||||
|
* **Maven Wrapper:** Utilize sempre o executável `./mvnw` (Linux/macOS) ou `mvnw.cmd` (Windows) embutido no projeto.
|
||||||
|
* **Git:** Com suporte a quebra de linha `LF`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Princípios de Segurança Invioláveis
|
||||||
|
|
||||||
|
Como este é um software de assinatura digital e custódia de credenciais criptográficas:
|
||||||
|
|
||||||
|
1. **Nunca exponha credenciais:** É estritamente proibido registrar, logar ou comitar PINs, senhas de arquivos A1, chaves privadas ou certificados de produção.
|
||||||
|
2. **Testes isolados:** Use fixtures mockadas ou certificados autoassinados temporários gerados dinamicamente nos testes.
|
||||||
|
3. **Loopback estrito:** O servidor CCSP escuta exclusivamente em `127.0.0.1:21325`. Nunca altere o bind para interfaces externas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Padrão de Commits
|
||||||
|
|
||||||
|
Adotamos a especificação [Conventional Commits](https://www.conventionalcommits.org/pt-br/v1.0.0/). Cada commit deve representar uma alteração coesa e atômica:
|
||||||
|
|
||||||
|
* `feat:` Nova funcionalidade no agente, SDK ou UI.
|
||||||
|
* `fix:` Correção de bug sem alteração de escopo.
|
||||||
|
* `docs:` Alterações exclusivas na documentação.
|
||||||
|
* `test:` Adição ou melhoria de suítes de testes.
|
||||||
|
* `refactor:` Alterações de código que não mudam o comportamento funcional.
|
||||||
|
* `chore:` Tarefas de manutenção de build, scripts ou dependências.
|
||||||
|
* `ci:` Alterações no pipeline de CI/CD.
|
||||||
|
|
||||||
|
Exemplo: `feat(core): add support for custom pkcs11 pin retry limit`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Validação Pré-Commit / Pré-PR
|
||||||
|
|
||||||
|
Antes de submeter um Pull Request, certifique-se de que o build completo e todas as análises de qualidade passam localmente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Executa compilação, testes, checagem JaCoCo, SpotBugs e ArchUnit:
|
||||||
|
./mvnw clean verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Critérios obrigatórios:
|
||||||
|
* Todos os testes unitários e de integração devem passar com sucesso.
|
||||||
|
* A cobertura mínima definida no `jacoco:check` de cada módulo deve ser mantida ou superada.
|
||||||
|
* Nenhum novo apontamento de segurança ou erro do SpotBugs deve ser introduzido.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Fluxo de Pull Requests
|
||||||
|
|
||||||
|
1. Crie uma branch descritiva a partir de `main` (ex: `feature/suporte-token-safenet` ou `fix/pades-timestamp-timeout`).
|
||||||
|
2. Mantenha os commits organizados e pequenos.
|
||||||
|
3. Abra o Pull Request preenchendo o template oficial do repositório em `.gitea/pull_request_template.md`.
|
||||||
|
4. Garanta que o pipeline de CI do Gitea Actions finalize com status verde.
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
> **Agente Desktop Nativo de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.**
|
> **Agente Desktop Nativo de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.**
|
||||||
|
|
||||||
[](https://openjdk.org/)
|
[](https://openjdk.org/)
|
||||||
[](https://openjfx.io/)
|
[](https://openjfx.io/)
|
||||||
[](docs/CCSP.md)
|
[](docs/CCSP.md)
|
||||||
[](https://www.gov.br/iti/pt-br)
|
[](https://www.gov.br/iti/pt-br)
|
||||||
[](#licença)
|
[](#licença)
|
||||||
|
|
||||||
O **CyberCore Signer** é uma solução desktop desenvolvida em Java 21+ LTS (homologada em Java 25 LTS) com interface gráfica moderna JavaFX e segurança criptográfica estrita JCA/JCE. Permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e validação de documentos utilizando certificados digitais padrão ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) conectados ou instalados na máquina do usuário.
|
O **CyberCore Signer** é uma solução desktop desenvolvida em Java 25 LTS com interface gráfica moderna JavaFX e segurança criptográfica estrita JCA/JCE. Permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e validação de documentos utilizando certificados digitais padrão ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) conectados ou instalados na máquina do usuário.
|
||||||
|
|
||||||
A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP localhost (`127.0.0.1:21325`), garantindo isolamento total de rede, consentimento visual explícito e preservação integral das chaves privadas na máquina local.
|
A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP localhost (`127.0.0.1:21325`), garantindo isolamento total de rede, consentimento visual explícito e preservação integral das chaves privadas na máquina local.
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ console.log('Certificado titular:', result.subject_name);
|
|||||||
### ☕ Para Desenvolvedores Desktop
|
### ☕ Para Desenvolvedores Desktop
|
||||||
|
|
||||||
#### Requisitos
|
#### Requisitos
|
||||||
* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin ou Corretto).
|
* **Java Development Kit (JDK):** Versão 25 LTS (OpenJDK, Temurin ou Corretto).
|
||||||
* **Maven:** 3.9+ (ou utilizar o `./mvnw` embutido).
|
* **Maven:** 3.9+ (ou utilizar o `./mvnw` embutido).
|
||||||
* **Sistema Operacional:** Windows 10/11 ou Linux x86_64.
|
* **Sistema Operacional:** Windows 10/11 ou Linux x86_64.
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ assets/
|
|||||||
|
|
||||||
1. **Ícones:**
|
1. **Ícones:**
|
||||||
- Todos estão no formato `.svg` padrão da W3C.
|
- Todos estão no formato `.svg` padrão da W3C.
|
||||||
|
- Consulte o guia completo de especificações em [docs/svg-design-guide.md](../docs/svg-design-guide.md) para tokens de cores, dimensões e boas práticas ao exportar do **Inkscape/Figma**.
|
||||||
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
|
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
|
||||||
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
|
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- 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"/>
|
<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 -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" 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="13" y1="22" x2="35" y2="22" 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"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta vermelha com sigla PDF -->
|
<!-- Etiqueta vermelha com sigla PDF perfeitamente centralizada -->
|
||||||
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="#d32f2f"/>
|
||||||
<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>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8.5px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">PDF</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 996 B After Width: | Height: | Size: 1.0 KiB |
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- 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"/>
|
<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 -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" 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="13" y1="22" x2="35" y2="22" 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"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta azul com sigla XML -->
|
<!-- Etiqueta azul com sigla XML perfeitamente centralizada -->
|
||||||
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
|
<rect x="11" y="31" 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>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central"><XML></text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.0 KiB |
@@ -52,12 +52,12 @@ echo " CYBERCORE SIGNER - AUTOMAÇÃO DE BUILD DO JAR"
|
|||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo -e "${COLOR_RESET}"
|
echo -e "${COLOR_RESET}"
|
||||||
|
|
||||||
# 1. Verificação e Auto-Detecção do Java (JDK 21+)
|
# 1. Verificação e Auto-Detecção do Java (JDK 25+)
|
||||||
if ! command -v java &>/dev/null; then
|
if ! command -v java &>/dev/null; then
|
||||||
POSSIBLE_JDKS=(
|
POSSIBLE_JDKS=(
|
||||||
|
"${HOME}/.jdks/jdk-25.0.4.1+1"
|
||||||
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
||||||
"/usr/lib/jvm/java-25-openjdk-amd64"
|
"/usr/lib/jvm/java-25-openjdk-amd64"
|
||||||
"/usr/lib/jvm/java-21-openjdk-amd64"
|
|
||||||
"/usr/lib/jvm/default-java"
|
"/usr/lib/jvm/default-java"
|
||||||
)
|
)
|
||||||
for candidate in "${POSSIBLE_JDKS[@]}"; do
|
for candidate in "${POSSIBLE_JDKS[@]}"; do
|
||||||
@@ -105,9 +105,15 @@ if [[ ! -f "${SOURCE_JAR}" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Copia para a raiz para facilitar o acesso
|
# 4. Copia para a raiz e para a pasta sdk/ para manter o ambiente sincronizado
|
||||||
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
|
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
|
||||||
|
|
||||||
|
if [[ -d "sdk" ]]; then
|
||||||
|
SDK_JAR="sdk/cybercore-signer.jar"
|
||||||
|
cp -f "${SOURCE_JAR}" "${SDK_JAR}"
|
||||||
|
echo -e "Cópia sincronizada para o SDK: ${COLOR_BOLD}${SDK_JAR}${COLOR_RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
|
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
Exclusões do SpotBugs / FindSecBugs.
|
Exclusões do SpotBugs / FindSecBugs para o CyberCore Signer.
|
||||||
Regra: toda exclusão deve ser específica (classe/padrão) e justificada em comentário.
|
Regra: toda exclusão deve ser específica (classe/padrão) e justificada em comentário.
|
||||||
-->
|
-->
|
||||||
<FindBugsFilter
|
<FindBugsFilter
|
||||||
@@ -9,15 +9,174 @@
|
|||||||
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
|
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
|
1. DTOs, Records e opções imutáveis que transferem dados estruturados (byte arrays, listas)
|
||||||
construtores canônicos (List.copyOf / Map.copyOf) mas são apontados por heurística do SpotBugs.
|
onde cópias defensivas estritas causariam overhead desnecessário de alocação criptográfica.
|
||||||
-->
|
-->
|
||||||
<Match>
|
<Match>
|
||||||
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
|
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
|
||||||
<Or>
|
<Or>
|
||||||
<Class name="br.com.cybercore.signer.core.diagnostics.DiagnosticReport"/>
|
<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.domain.SigningResult"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.domain.SigningKeyMaterial"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.protocol.CcspRequest"/>
|
||||||
<Class name="br.com.cybercore.signer.core.protocol.CcspResponse"/>
|
<Class name="br.com.cybercore.signer.core.protocol.CcspResponse"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.validation.ValidationResult"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.validation.SignatureValidationService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.xml.XmlSignatureOptions"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.PdfSignatureOptions"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.PdfSignatureOptions$Builder"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.seal.PdfSealData"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.provider.Pkcs11CertificateProvider"/>
|
||||||
|
<Package name="~br\.com\.cybercore\.signer\.ui\.view.*"/>
|
||||||
</Or>
|
</Or>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
1b. Referência estática ao Stage principal ativo para apresentação de diálogos modais pelo prompt listener.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="EI_EXPOSE_STATIC_REP2"/>
|
||||||
|
<Class name="br.com.cybercore.signer.ui.prompt.JavaFxPromptListener"/>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
2. Singletons locais e serviços de sessão que utilizam métodos sincronizados para controle
|
||||||
|
de concorrência thread-safe em memória local do agente desktop.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="USO_UNSAFE_METHOD_SYNCHRONIZATION,USO_UNSAFE_STATIC_METHOD_SYNCHRONIZATION"/>
|
||||||
|
<Or>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.SecurePinStorage"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.PinSessionManager"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.ApplicationRegistry"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.config.AgentConfigurationService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.log.AppLogBuffer"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.seal.CustomSealStorage"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.provider.CertificateManager"/>
|
||||||
|
</Or>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
3. Construtores de Singletons com visibilidade default/package-private para permitir
|
||||||
|
instanciação isolada durante testes unitários.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR"/>
|
||||||
|
<Or>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.ApplicationRegistry"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.PinSessionManager"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.SecurePinStorage"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.config.AgentConfigurationService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.log.AppLogBuffer"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.seal.CustomSealStorage"/>
|
||||||
|
</Or>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
4. Exceções lançadas em construtores de serviços criptográficos e diálogos que necessitam
|
||||||
|
validar algoritmos ou dependências durante a instanciação (fail-fast).
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="CT_CONSTRUCTOR_THROW"/>
|
||||||
|
<Or>
|
||||||
|
<Class name="br.com.cybercore.signer.core.xml.XmlSigningService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.PdfSigningService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.PdfSigningService$PadesBouncyCastleSigner"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.crypto.SigningService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.protocol.CcspServer"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.crypto.tsa.TsaClient"/>
|
||||||
|
<Class name="br.com.cybercore.signer.ui.tray.SystemTrayManager"/>
|
||||||
|
<Package name="~br\.com\.cybercore\.signer\.ui\.view.*"/>
|
||||||
|
</Or>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
5. Tratamento de exceções amplo (catch Exception) intencional em rotinas defensivas de
|
||||||
|
validação criptográfica e telemetria para não abortar o processo desktop.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="REC_CATCH_EXCEPTION"/>
|
||||||
|
<Or>
|
||||||
|
<Class name="br.com.cybercore.signer.core.validation.SignatureValidationService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.SecurePinStorage"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.protocol.CcspServer"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.protocol.CcspServer$SignXmlHandler"/>
|
||||||
|
<Class name="br.com.cybercore.signer.ui.assets.AssetManager"/>
|
||||||
|
<Package name="~br\.com\.cybercore\.signer\.ui\.view.*"/>
|
||||||
|
</Or>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
6. Instanciação única de SecureRandom em métodos locais para geração de nonces efêmeros.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="DMI_RANDOM_USED_ONLY_ONCE"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.SecurePinStorage"/>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
7. Validação de saída de diretório (mkdirs) e condições de contorno defensivas em parsing.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="RV_RETURN_VALUE_IGNORED_BAD_PRACTICE,UC_USELESS_CONDITION,NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
|
||||||
|
<Or>
|
||||||
|
<Class name="br.com.cybercore.signer.core.xml.XmlSigningService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.PdfSigningService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.validation.SignatureValidationService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.config.AgentConfigurationService"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.protocol.CcspServer"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.seal.CustomSealStorage"/>
|
||||||
|
<Package name="~br\.com\.cybercore\.signer\.ui\.view.*"/>
|
||||||
|
</Or>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
8. Transformações locais de XML (FindSecBugs) utilizadas estritamente para montagem de
|
||||||
|
assinaturas digitais XAdES locais sem ingestão de fontes remotas não confiáveis.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="XXE_DTD_TRANSFORM_FACTORY,XXE_XSLT_TRANSFORM_FACTORY"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.xml.XmlSigningService"/>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
9. Campos estáticos públicos e gerenciamento de ciclo de vida da interface gráfica JavaFX,
|
||||||
|
servidor HTTP de loopback e do System Tray (incluindo saída voluntária System.exit).
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="MS_EXPOSE_REP,ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD,DM_EXIT,DE_MIGHT_IGNORE,IS2_INCONSISTENT_SYNC,RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE,VA_FORMAT_STRING_USES_NEWLINE"/>
|
||||||
|
<Or>
|
||||||
|
<Class name="br.com.cybercore.signer.core.provider.WindowsCertificateStoreProvider"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.ApplicationRegistry"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.PinSessionManager"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.security.SecurePinStorage"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.pdf.seal.CustomSealStorage"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.protocol.CcspServer"/>
|
||||||
|
<Class name="br.com.cybercore.signer.ui.CyberCoreDesktopApplication"/>
|
||||||
|
<Class name="br.com.cybercore.signer.ui.tray.SystemTrayManager"/>
|
||||||
|
<Class name="br.com.cybercore.signer.ui.assets.AssetManager"/>
|
||||||
|
<Package name="~br\.com\.cybercore\.signer\.ui\.view.*"/>
|
||||||
|
</Or>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
10. Flags booleanas primitivas de linha de comando inicializadas no bootstrap da aplicação.
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="AT_STALE_THREAD_WRITE_OF_PRIMITIVE"/>
|
||||||
|
<Class name="br.com.cybercore.signer.app.CyberCoreSignerApplication"/>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
11. Carregamento de driver PKCS#11 (middleware de fabricante de hardware criptográfico)
|
||||||
|
apontando para bibliotecas dinâmicas do sistema operacional (.dll no Windows ou .so no Linux).
|
||||||
|
-->
|
||||||
|
<Match>
|
||||||
|
<Bug pattern="PATH_TRAVERSAL_IN"/>
|
||||||
|
<Class name="br.com.cybercore.signer.core.provider.Pkcs11CertificateProvider"/>
|
||||||
|
</Match>
|
||||||
|
|
||||||
</FindBugsFilter>
|
</FindBugsFilter>
|
||||||
|
|||||||
@@ -152,11 +152,20 @@ Solicita a assinatura digital local do digest informado com o certificado escolh
|
|||||||
"certificate_hint": "win-4340656a",
|
"certificate_hint": "win-4340656a",
|
||||||
"document_name": "contrato_prestacao_servicos.pdf",
|
"document_name": "contrato_prestacao_servicos.pdf",
|
||||||
"document_type": "PDF",
|
"document_type": "PDF",
|
||||||
|
"document_size": 1474560,
|
||||||
|
"origin": "https://meusistema.empresa.com.br",
|
||||||
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
||||||
"expires_at": "2026-10-11T20:50:00Z"
|
"expires_at": "2026-10-11T20:50:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Garantia WYSIWYS (What You See Is What You Sign):**
|
||||||
|
> O agente CyberCore Signer apresenta ao usuário, antes de qualquer operação criptográfica:
|
||||||
|
> - **Aplicação Solicitante e Origem Web:** Exibe a URL de origem da página web chamadora para prevenir ataques de personificação (Spoofing).
|
||||||
|
> - **Nome e Tamanho do Documento:** Se informados via `document_name` e `document_size` (em bytes), são apresentados de forma legível (ex: `1.4 MB`).
|
||||||
|
> - **Hash do Documento (SHA-256 Digest):** Formatado em blocos legíveis de 4 caracteres (ex: `A1B2 C3D4 ...`) em tipografia monoespaçada para fácil conferência humana.
|
||||||
|
> - **Certificado e Dispositivo:** Exibe titular, CPF/CNPJ, tipo A1/A3 e algoritmo de assinatura.
|
||||||
|
|
||||||
#### Resposta de Sucesso (`200 OK`)
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -348,22 +357,52 @@ Quando qualquer operação falha, o servidor local CCSP/1 responde com um objeto
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mapeamento de Status HTTP
|
### Lista Completa de Códigos de Erro Estruturados
|
||||||
|
|
||||||
| Código HTTP | Erro Típico | Descrição |
|
Todos os códigos de erro são estáveis, estruturados e interoperáveis entre o backend Java e o SDK JavaScript:
|
||||||
|
|
||||||
|
| Código de Erro (`error_code`) | Descrição e Causa Raiz | Mapeamento no SDK |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `400 Bad Request` | `USER_CANCELLED`, `PIN_INCORRECT`, `SIGNATURE_FAILED` | Requisição malformada, cancelamento pelo usuário ou falha de PIN |
|
| `CERTIFICATE_NOT_FOUND` | Certificado não encontrado pelo ID ou hint especificado | `CertificateNotFoundError` |
|
||||||
| `401 Unauthorized` | `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou sem cabeçalho `X-CyberCore-AppId` |
|
| `CERTIFICATE_EXPIRED` | Certificado ultrapassou a data limite de validade (`notAfter`) | `CertificateExpiredError` |
|
||||||
| `403 Forbidden` | `INVALID_ORIGIN` | Cabeçalho `Origin` não confere com o domínio cadastrado |
|
| `CERTIFICATE_INVALID` | Certificado corrompido, revogado ou com estrutura X.509 inválida | `CertificateInvalidError` |
|
||||||
| `404 Not Found` | `CERTIFICATE_NOT_FOUND`, `TOKEN_NOT_FOUND` | Certificado ou dispositivo físico ausente |
|
| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível, protegida por hardware desconectado ou ausente | `PrivateKeyNotAvailableError` |
|
||||||
| `409 Conflict` | `REPLAY_DETECTED` | Nonce já utilizado anteriormente na janela de validação |
|
| `TOKEN_NOT_FOUND` | Nenhum token USB, smart card ou leitora física detectada | `TokenNotFoundError` |
|
||||||
| `422 Unprocessable`| `REQUEST_EXPIRED`, `CERTIFICATE_EXPIRED` | Parâmetros sintaticamente válidos mas expirados temporalmente |
|
| `TOKEN_REMOVED` | O dispositivo físico foi desconectado durante a operação | `TokenNotFoundError` |
|
||||||
| `500 Server Error` | `PKCS11_ERROR`, `PROVIDER_NOT_AVAILABLE` | Falha interna no middleware do fabricante ou driver criptográfico |
|
| `PIN_REQUIRED` | O hardware criptográfico A3 exige digitação de PIN para desbloqueio | `PinRequiredError` |
|
||||||
| `503 Service Unavail`| `TOKEN_REMOVED`, `COMMUNICATION_ERROR` | Hardware desconectado ou serviço em recuperação |
|
| `PIN_INCORRECT` | O PIN digitado pelo usuário foi rejeitado pelo token (risco de bloqueio de PUK) | `PinIncorrectError` |
|
||||||
|
| `PKCS11_ERROR` | Falha interna no driver, middleware PKCS#11 ou DLL do fabricante | `CyberCoreError` |
|
||||||
|
| `SIGNATURE_FAILED` | Erro matemático na cifra assimétrica ou payload malformado | `SignatureFailedError` |
|
||||||
|
| `APPLICATION_NOT_AUTHORIZED` | A aplicação web solicitante não possui autorização concedida pelo usuário | `ApplicationNotAuthorizedError` |
|
||||||
|
| `REQUEST_EXPIRED` | O timestamp `expires_at` da requisição foi ultrapassado (timeout) | `RequestExpiredError` |
|
||||||
|
| `REPLAY_DETECTED` | O identificador `nonce` já foi processado anteriormente (ataque de replay) | `ReplayDetectedError` |
|
||||||
|
| `INVALID_ORIGIN` | O cabeçalho `Origin` não confere com os domínios homologados para o AppId | `InvalidOriginError` |
|
||||||
|
| `INVALID_HOST` | O cabeçalho `Host` foi adulterado (tentativa de DNS Rebinding) | `CyberCoreError` |
|
||||||
|
| `UNSUPPORTED_ALGORITHM` | Algoritmo criptográfico solicitado não é suportado pelo provider | `UnsupportedAlgorithmError` |
|
||||||
|
| `USER_CANCELLED` | O usuário clicou explicitamente em 'Cancelar' no diálogo nativo do sistema | `UserCancelledError` |
|
||||||
|
| `PROVIDER_NOT_AVAILABLE` | Provedor criptográfico nativo indisponível na plataforma atual | `ProviderNotAvailableError` |
|
||||||
|
| `COMMUNICATION_ERROR` | Falha na camada de transporte HTTP local do loopback | `CyberCoreError` |
|
||||||
|
| `AGENT_NOT_RUNNING` | O agente desktop não está rodando em 127.0.0.1:21325 (lado SDK) | `AgentNotRunningError` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Diagrama de Sequência Completo
|
## 5. Especificação JSON Schema e Versionamento
|
||||||
|
|
||||||
|
O contrato canônico e exaustivo de todos os endpoints e payloads do protocolo CCSP/1 está formalizado no arquivo:
|
||||||
|
- [docs/ccsp-v1.schema.json](file:///var/www/cybercore/cybercore-signer/docs/ccsp-v1.schema.json)
|
||||||
|
|
||||||
|
### Política de Versionamento e Retrocompatibilidade
|
||||||
|
|
||||||
|
1. **Identificador de Versão Maior (`version: "1"`):**
|
||||||
|
- O campo `version` em todos os requests e responses identifica a versão maior do protocolo. O agente desktop garante compatibilidade retroativa com todas as revisões do CCSP/1.
|
||||||
|
2. **Evolução Aditiva (Não Quebrante):**
|
||||||
|
- Novos campos opcionais de requisição e novos metadados informativos de resposta podem ser adicionados dentro da versão CCSP/1 sem quebrar clientes existentes. Clientes e servidores devem ignorar propriedades adicionais desconhecidas (`additionalProperties` tolerantes).
|
||||||
|
3. **Mudança Quebrante (Nova Versão Maior):**
|
||||||
|
- Qualquer alteração na semântica de autenticação, remoção de campos obrigatórios ou mudança estrutural de URLs exigirá a introdução de uma nova versão de protocolo (ex: CCSP/2 com prefixo `/ccsp/v2/`). O agente manterá os endpoints `/ccsp/v1/` operacionais em paralelo para garantir transição suave.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Diagrama de Sequência Completo
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Diagnóstico: Janela Principal no Windows 11 (Bisseção e Monitoramento)
|
||||||
|
|
||||||
|
Este documento descreve o procedimento de diagnóstico para isolar a causa de ocultamento nativo da janela principal no Windows 11 ao passar o mouse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Monitor Nativo Win32/DWM (`scripts/diag-window.ps1`)
|
||||||
|
|
||||||
|
O script [scripts/diag-window.ps1](file:///var/www/cybercore/cybercore-signer/scripts/diag-window.ps1) inspeciona a cada 300ms a janela nativa via P/Invoke da Win32 API e DWM:
|
||||||
|
- `IsWindowVisible`: se a janela está visível perante o subsistema Win32;
|
||||||
|
- `DWMWA_CLOAKED`: se o gerenciador gráfico do Windows 11 ocultou a composição;
|
||||||
|
- `IsIconic`: se foi minimizada;
|
||||||
|
- `GetWindowRect`: coordenadas reais (detecta se foi deslocada para fora da tela).
|
||||||
|
|
||||||
|
### Como executar o monitor:
|
||||||
|
Em uma janela separada do PowerShell:
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Roteiro de Bisseção (Fase 2)
|
||||||
|
|
||||||
|
Execute cada um dos comandos abaixo individualmente (garantindo antes que instâncias anteriores foram encerradas com `Stop-Process -Name java -Force` se necessário):
|
||||||
|
|
||||||
|
### Teste A: Desativar Middleware PKCS#11 / SafeSign (`aetpkss1.dll`)
|
||||||
|
Isola hooks nativos do SafeSign e middlewares A3:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-pkcs11=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
*Se a janela NÃO sumir ao passar o mouse, a causa é o hook nativo do middleware PKCS#11.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste B: Plain Stage (Janela livre sem `setResizable(false)` e sem limites min/max)
|
||||||
|
Isola o bug de composição do DWM / Glass no Windows 11 com janelas de tamanho fixo:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.plain-stage=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
*Se a janela NÃO sumir ao passar o mouse, a causa é a restrição fixa de tamanho no Windows 11.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste C: Desativar Efeitos de Sombra no Mouse (`-fx-effect`)
|
||||||
|
Isola recálculo de dropshadow e troca de cursor:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-effects=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste D: Desativar Monitor Dinâmico de Smart Cards (PC/SC)
|
||||||
|
Isola threads em segundo plano de `javax.smartcardio`:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-smartcard-watcher=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste E: Desativar Windows Certificate Store (Windows-MY)
|
||||||
|
Isola inicialização de CryptoAPI / SunMSCAPI:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-wincert=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Índice Unificado e Guias Técnicos do Agente Desktop e Protocolo CCSP**
|
> **Índice Unificado e Guias Técnicos do Agente Desktop e Protocolo CCSP**
|
||||||
|
|
||||||
Bem-vindo à documentação oficial do **CyberCore Signer**. O agente foi desenvolvido em Java 21+ LTS (Java 25 LTS) com interface JavaFX e segurança criptográfica nativa JCA/JCE, permitindo que aplicações web autorizadas realizem operações de assinatura digital e autenticação com certificados ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) mantendo as chaves privadas estritamente no dispositivo do usuário.
|
Bem-vindo à documentação oficial do **CyberCore Signer**. O agente foi desenvolvido em Java 25 LTS com interface JavaFX e segurança criptográfica nativa JCA/JCE, permitindo que aplicações web autorizadas realizem operações de assinatura digital e autenticação com certificados ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) mantendo as chaves privadas estritamente no dispositivo do usuário.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ Se você precisa implantar, homologar ou validar os aspectos de conformidade e s
|
|||||||
|
|
||||||
* **[Modelo de Segurança e Privacidade](security.md):** Diretrizes de menor privilégio, proteção contra CSRF e DNS Rebinding, descarte seguro de senhas e PIN em memória e conformidade com a LGPD.
|
* **[Modelo de Segurança e Privacidade](security.md):** Diretrizes de menor privilégio, proteção contra CSRF e DNS Rebinding, descarte seguro de senhas e PIN em memória e conformidade com a LGPD.
|
||||||
* **[Instalação e Empacotamento Nativo](installation.md):** Criação de instaladores autocontidos para Windows (MSI/EXE) via `jpackage`, requisitos do sistema e parâmetros de instalação corporativa silenciosa.
|
* **[Instalação e Empacotamento Nativo](installation.md):** Criação de instaladores autocontidos para Windows (MSI/EXE) via `jpackage`, requisitos do sistema e parâmetros de instalação corporativa silenciosa.
|
||||||
|
* **[Estratégia de Versionamento e Release](release-and-versioning.md):** Padrão SemVer 2.0.0, plano progressivo de cobertura JaCoCo e roteiro para corte da primeira tag v0.1.0.
|
||||||
* **[Diagnóstico e Troubleshooting](diagnostics.md):** Subsistema de auto-teste, checklist de integridade de runtime e guia de resolução de problemas comuns de hardware/middleware.
|
* **[Diagnóstico e Troubleshooting](diagnostics.md):** Subsistema de auto-teste, checklist de integridade de runtime e guia de resolução de problemas comuns de hardware/middleware.
|
||||||
* **[Limitações Técnicas Declaradas](limitations.md):** Declaração formal de requisitos de drivers, limitações de hardware mono-thread e dependências de sistema operacional (em conformidade com a Regra 23 do projeto).
|
* **[Limitações Técnicas Declaradas](limitations.md):** Declaração formal de requisitos de drivers, limitações de hardware mono-thread e dependências de sistema operacional (em conformidade com a Regra 23 do projeto).
|
||||||
|
|
||||||
@@ -56,9 +57,15 @@ Se você precisa implantar, homologar ou validar os aspectos de conformidade e s
|
|||||||
| [`security.md`](security.md) | Políticas de isolamento, memória e privacidade | Segurança & Compliance |
|
| [`security.md`](security.md) | Políticas de isolamento, memória e privacidade | Segurança & Compliance |
|
||||||
| [`diagnostics.md`](diagnostics.md) | Auto-testes e guia de resolução de problemas | Suporte & Operações |
|
| [`diagnostics.md`](diagnostics.md) | Auto-testes e guia de resolução de problemas | Suporte & Operações |
|
||||||
| [`installation.md`](installation.md) | Empacotamento com `jpackage` e instaladores MSI | DevOps & Usuários Finais |
|
| [`installation.md`](installation.md) | Empacotamento com `jpackage` e instaladores MSI | DevOps & Usuários Finais |
|
||||||
|
| [`release-and-versioning.md`](release-and-versioning.md) | Estratégia SemVer, metas JaCoCo e corte de tags | DevOps & Governança |
|
||||||
|
| [`compliance-testing.md`](compliance-testing.md) | Guia de Conformidade ICP-Brasil e Verificador ITI | Compliance & QA |
|
||||||
|
| [`compatibility.md`](compatibility.md) | Matriz de compatibilidade de hardware A3 e SoftHSM2 | Suporte & Hardware |
|
||||||
| [`limitations.md`](limitations.md) | Dependências de drivers e limitações declaradas | Todos |
|
| [`limitations.md`](limitations.md) | Dependências de drivers e limitações declaradas | Todos |
|
||||||
| [`development-mode.md`](development-mode.md) | Parâmetro `--dev` e certificados de teste | Desenvolvedores Desktop |
|
| [`development-mode.md`](development-mode.md) | Parâmetro `--dev` e certificados de teste | Desenvolvedores Desktop |
|
||||||
| [`sdk.md`](sdk.md) | Guia completo da biblioteca JavaScript | Desenvolvedores Web |
|
| [`sdk.md`](sdk.md) | Guia completo da biblioteca JavaScript | Desenvolvedores Web |
|
||||||
|
| [`browser-compatibility.md`](browser-compatibility.md) | Compatibilidade de navegadores, HTTPS para loopback e Private Network Access (PNA) | Integradores Web & Segurança |
|
||||||
|
| [`ccsp-v1.schema.json`](ccsp-v1.schema.json) | JSON Schema formal e versionado do protocolo CCSP/1 | Integradores & QA |
|
||||||
|
| [`sdk-release-process.md`](sdk-release-process.md) | Processo de publicação e release do SDK no registry npm privado | DevOps & Engenharia |
|
||||||
| [`serpro-layout-and-batch.md`](serpro-layout-and-batch.md) | Design de interface e processamento em lote | UI/UX & Desenvolvedores |
|
| [`serpro-layout-and-batch.md`](serpro-layout-and-batch.md) | Design de interface e processamento em lote | UI/UX & Desenvolvedores |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Especificação da Arquitetura Modular, Modelo de Threads e Fluxo Criptográfico**
|
> **Especificação da Arquitetura Modular, Modelo de Threads e Fluxo Criptográfico**
|
||||||
|
|
||||||
O **CyberCore Signer** foi concebido a partir de princípios de alta coesão, baixo acoplamento e estrito isolamento de camadas. O software é desenvolvido em **Java 21+ LTS (Java 25 LTS)**, utilizando o runtime modular do JDK, JavaFX para a camada de apresentação e os provedores JCA/JCE da plataforma para operações criptográficas seguras.
|
O **CyberCore Signer** foi concebido a partir de princípios de alta coesão, baixo acoplamento e estrito isolamento de camadas. O software é desenvolvido em **Java 25 LTS**, utilizando o runtime modular do JDK, JavaFX para a camada de apresentação e os provedores JCA/JCE da plataforma para operações criptográficas seguras.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ O módulo de núcleo é totalmente independente de JavaFX ou bibliotecas visuais
|
|||||||
|
|
||||||
### 1.2 Módulo `cybercore-ui` (Apresentação JavaFX)
|
### 1.2 Módulo `cybercore-ui` (Apresentação JavaFX)
|
||||||
Responsável por toda a interação com o usuário:
|
Responsável por toda a interação com o usuário:
|
||||||
* **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade 3x3 de operações rápidas (PDF, XML, Arquivo Genérico, Assinatura em Lote, Assinatura Atachada, Co-Assinatura, Carimbo de Tempo, Designer de Selos, Validador) e coluna lateral de utilitários (Logs em tempo real, Configurações e Diagnóstico).
|
* **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade de operações rápidas (PDF, XML, Assinatura Atachada, Designer de Selos, Validar Assinatura, Sair do Agente) e coluna lateral de utilitários (Logs em tempo real, Configurações, Tutorial, Certificados e Sobre).
|
||||||
* **Diálogos de Consentimento e PIN:** Modais nativos de aprovação visual para requisições do protocolo CCSP/1 (`SignatureConfirmationDialog`, `ApplicationAuthorizeDialog`) e coleta segura de PIN de tokens (`A1PasswordDialog`).
|
* **Diálogos de Consentimento e PIN:** Modais nativos de aprovação visual para requisições do protocolo CCSP/1 (`SignatureConfirmationDialog`, `ApplicationAuthorizeDialog`) e coleta segura de PIN de tokens (`A1PasswordDialog`).
|
||||||
* **Designer de Selos Digitais (`SealDesignerDialog`):** Ferramenta com Live Preview para criação de selos visuais com controle de cores, estilo de brasão, exibição de nome completo sem truncamento e privacidade de CPF.
|
* **Designer de Selos Digitais (`SealDesignerDialog`):** Ferramenta com Live Preview para criação de selos visuais com controle de cores, estilo de brasão, exibição de nome completo sem truncamento e privacidade de CPF.
|
||||||
* **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
|
* **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Compatibilidade de Navegadores e Acesso de Rede Privada (CCSP/1)
|
||||||
|
|
||||||
|
Este documento detalha o funcionamento, as garantias de segurança e os requisitos de compatibilidade entre aplicações web (servidas sob **HTTPS**) e o agente desktop **CyberCore Signer** em loopback local (`http://127.0.0.1:21325`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Contexto Arquitetural: HTTPS para Loopback HTTP
|
||||||
|
|
||||||
|
Tradicionalmente, navegadores modernos impedem o carregamento de recursos inseguros (`http://`) a partir de contextos seguros (`https://`), mecanismo conhecido como **Mixed Content**.
|
||||||
|
|
||||||
|
No entanto, para agentes de assinatura digital locais e periféricos de hardware, o consórcio **W3C (Secure Contexts Specification)** e os motores de renderização modernos tratam o loopback IPv4 (`127.0.0.1`) e `localhost` como **Origens Potencialmente Confiáveis (*Potentially Trustworthy Origins*)**.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant Web as Aplicação Web (HTTPS)
|
||||||
|
participant Browser as Navegador (Chrome/Firefox/Safari)
|
||||||
|
participant Agent as CyberCore Signer (127.0.0.1:21325)
|
||||||
|
|
||||||
|
Note over Web,Agent: 1. Preflight CORS + Private Network Access (PNA)
|
||||||
|
Web->>Browser: fetch("http://127.0.0.1:21325/ccsp/v1/sign", { method: "POST" })
|
||||||
|
Browser->>Agent: OPTIONS /ccsp/v1/sign<br/>Origin: https://meusistema.com.br<br/>Access-Control-Request-Private-Network: true
|
||||||
|
Agent-->>Browser: HTTP 204 No Content<br/>Access-Control-Allow-Origin: *<br/>Access-Control-Allow-Private-Network: true<br/>Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||||
|
Note over Browser,Agent: 2. Requisição Real CCSP/1
|
||||||
|
Browser->>Agent: POST /ccsp/v1/sign (JSON Digest)
|
||||||
|
Agent-->>Browser: HTTP 200 OK (Assinatura Base64)
|
||||||
|
Browser-->>Web: Retorno Promise Resolvida
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Private Network Access (PNA / W3C)
|
||||||
|
|
||||||
|
O **Private Network Access (PNA)** é uma especificação de segurança do W3C (adotada inicialmente pelo motor Chromium a partir do Chrome 98+) com o objetivo de proteger a rede local contra ataques de Cross-Site Request Forgery (CSRF).
|
||||||
|
|
||||||
|
### Como a especificação classifica as redes:
|
||||||
|
1. **Pública (*public*):** A internet pública (ex: `https://meusistema.com.br`).
|
||||||
|
2. **Privada (*private*):** Redes locais RFC 1918 (ex: `192.168.x.x`, `10.x.x.x`).
|
||||||
|
3. **Local (*local*):** A máquina do próprio usuário (`127.0.0.1`, `localhost`).
|
||||||
|
|
||||||
|
Quando uma página em um espaço de IP mais público solicita um recurso em um espaço mais restrito (como `public` $\rightarrow$ `local`), o navegador dispara uma requisição preliminar **Preflight PNA**.
|
||||||
|
|
||||||
|
### Headers Enviados pelo Navegador:
|
||||||
|
```http
|
||||||
|
OPTIONS /ccsp/v1/sign HTTP/1.1
|
||||||
|
Host: 127.0.0.1:21325
|
||||||
|
Origin: https://meusistema.com.br
|
||||||
|
Access-Control-Request-Method: POST
|
||||||
|
Access-Control-Request-Headers: Content-Type, X-CyberCore-AppId
|
||||||
|
Access-Control-Request-Private-Network: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resposta Padrão do CyberCore Signer (`CcspServer`):
|
||||||
|
O servidor do agente desktop intercepta requisições `OPTIONS` e responde imediatamente com:
|
||||||
|
```http
|
||||||
|
HTTP/1.1 204 No Content
|
||||||
|
Access-Control-Allow-Origin: *
|
||||||
|
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||||
|
Access-Control-Allow-Headers: Content-Type, Authorization, X-CyberCore-AppId, Origin, Accept
|
||||||
|
Access-Control-Allow-Private-Network: true
|
||||||
|
Access-Control-Max-Age: 86400
|
||||||
|
Connection: close
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> O cabeçalho `Access-Control-Allow-Private-Network: true` é fundamental para autorizar o navegador a enviar requisições privadas originadas de sites HTTPS públicos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Matriz de Compatibilidade de Navegadores
|
||||||
|
|
||||||
|
| Navegador | Versão Mínima | Suporte HTTPS $\rightarrow$ 127.0.0.1 | Comportamento PNA | Requer Ação do Usuário? |
|
||||||
|
| :--- | :---: | :---: | :---: | :---: |
|
||||||
|
| **Google Chrome** | 88+ | ✅ Suportado | PNA ativo com preflight | Não (automático) |
|
||||||
|
| **Microsoft Edge** | 88+ | ✅ Suportado | PNA ativo com preflight | Não (automático) |
|
||||||
|
| **Mozilla Firefox** | 84+ | ✅ Suportado | Trata 127.0.0.1 como Secure Context | Não (automático) |
|
||||||
|
| **Opera / Brave** | Recentes | ✅ Suportado | Idêntico ao Chromium (ver nota Brave) | Pode exigir ajuste no Shields |
|
||||||
|
| **Apple Safari** | 15.4+ (macOS) | ✅ Suportado | Trata loopback como seguro | Não |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Casos de Borda e Soluções (Troubleshooting)
|
||||||
|
|
||||||
|
### 4.1. Navegador Brave (Brave Shields)
|
||||||
|
O Brave possui um mecanismo agressivo de proteção que pode bloquear preflights para `127.0.0.1`.
|
||||||
|
- **Sintoma:** O SDK lança `AgentNotRunningError`, mesmo com o agente em execução.
|
||||||
|
- **Resolução:** O usuário deve clicar no ícone do escudo (Shields) na barra de endereços da aplicação e desativar temporariamente o bloqueio para a origem ou permitir conexões locais nas configurações (`brave://settings/shields`).
|
||||||
|
|
||||||
|
### 4.2. Extensões de Privacidade e AdBlockers
|
||||||
|
Extensões como *uBlock Origin*, *Privacy Badger* ou *NoScript* podem conter regras genéricas de bloqueio para portas locais.
|
||||||
|
- **Resolução:** Adicionar a URL da sua aplicação web à lista de permissões (*allowlist*) da extensão.
|
||||||
|
|
||||||
|
### 4.3. Softwares de Antivírus e Endpoint Protection
|
||||||
|
Alguns antivírus com módulo de "Proteção Web" ou "Inspeção HTTPS" tentam atuar como proxy transparente no loopback local:
|
||||||
|
- **Resolução:** Configurar o antivírus para excluir a porta `21325` da inspeção de tráfego local.
|
||||||
|
|
||||||
|
### 4.4. Políticas Corporativas de Grupo (GPO / Chromium Enterprise)
|
||||||
|
Em ambientes corporativos restritos, administradores de rede podem bloquear chamadas a redes privadas via política:
|
||||||
|
- Política Chromium: `InsecurePrivateNetworkRequestsAllowed` ou `InsecurePrivateNetworkRequestsAllowedForUrls`.
|
||||||
|
- **Resolução:** Adicionar a URL do portal web corporativo na política `InsecurePrivateNetworkRequestsAllowedForUrls`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Boas Práticas para Desenvolvedores Web
|
||||||
|
|
||||||
|
1. **Use sempre o SDK `@cybercore/signer-sdk`:** Ele gerencia timeouts, fallbacks de preflight e detecção de conectividade.
|
||||||
|
2. **Utilize `isInstalled()` antes de iniciar fluxos de assinatura:**
|
||||||
|
```javascript
|
||||||
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
const signer = new CyberCoreSigner();
|
||||||
|
|
||||||
|
if (!(await signer.isInstalled())) {
|
||||||
|
mostrarModalInstalacaoAgente();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **Capture `AgentNotRunningError` e exiba o guia de ajuda:**
|
||||||
|
```javascript
|
||||||
|
import { AgentNotRunningError } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signer.connect();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AgentNotRunningError) {
|
||||||
|
console.info(err.actionableGuidance);
|
||||||
|
exibirAjudaAoUsuario(err.actionableGuidance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://cybercore.com.br/schemas/ccsp-v1.json",
|
||||||
|
"title": "CyberCore Signing Protocol v1 (CCSP/1) Specification",
|
||||||
|
"description": "Especificação formal do protocolo JSON para comunicação local entre aplicações web e o agente desktop CyberCore Signer.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["1"],
|
||||||
|
"description": "Versão do protocolo CCSP (sempre '1' para CCSP/1)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"ErrorCode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"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",
|
||||||
|
"INVALID_HOST",
|
||||||
|
"UNSUPPORTED_ALGORITHM",
|
||||||
|
"USER_CANCELLED",
|
||||||
|
"PROVIDER_NOT_AVAILABLE",
|
||||||
|
"COMMUNICATION_ERROR",
|
||||||
|
"AGENT_NOT_RUNNING",
|
||||||
|
"TIMEOUT_ERROR",
|
||||||
|
"BAD_REQUEST",
|
||||||
|
"METHOD_NOT_ALLOWED"
|
||||||
|
],
|
||||||
|
"description": "Códigos formais de erro estruturados do protocolo CCSP/1."
|
||||||
|
},
|
||||||
|
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "error_code", "message"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["error"] },
|
||||||
|
"error_code": { "$ref": "#/definitions/ErrorCode" },
|
||||||
|
"message": { "type": "string" },
|
||||||
|
"request_id": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"StatusResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "agent", "agent_version", "os", "timestamp"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["ready", "busy", "error"] },
|
||||||
|
"agent": { "type": "string" },
|
||||||
|
"agent_version": { "type": "string" },
|
||||||
|
"os": { "type": "string" },
|
||||||
|
"timestamp": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"AuthorizeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "application_id"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"application_id": { "type": "string", "pattern": "^[0-9a-fA-F-]{36}$" },
|
||||||
|
"name": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"AuthorizeResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "application_id"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["authorized"] },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"message": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"CertificateItem": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "alias", "type", "can_sign", "status"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"alias": { "type": "string" },
|
||||||
|
"common_name": { "type": ["string", "null"] },
|
||||||
|
"subject_dn": { "type": ["string", "null"] },
|
||||||
|
"issuer_dn": { "type": ["string", "null"] },
|
||||||
|
"type": { "type": "string", "enum": ["A1", "A3"] },
|
||||||
|
"provider_id": { "type": ["string", "null"] },
|
||||||
|
"device_name": { "type": ["string", "null"] },
|
||||||
|
"valid_from": { "type": ["string", "null"] },
|
||||||
|
"valid_to": { "type": ["string", "null"] },
|
||||||
|
"serial_number": { "type": ["string", "null"] },
|
||||||
|
"cpf": { "type": ["string", "null"] },
|
||||||
|
"cnpj": { "type": ["string", "null"] },
|
||||||
|
"can_sign": { "type": "boolean" },
|
||||||
|
"status": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"CertificatesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "certificates"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["success"] },
|
||||||
|
"certificates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/CertificateItem" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SealThemeItem": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"description": { "type": ["string", "null"] },
|
||||||
|
"width": { "type": "number" },
|
||||||
|
"height": { "type": "number" },
|
||||||
|
"primary_color": { "type": ["string", "null"] },
|
||||||
|
"secondary_color": { "type": ["string", "null"] },
|
||||||
|
"background_color": { "type": ["string", "null"] },
|
||||||
|
"border_color": { "type": ["string", "null"] },
|
||||||
|
"text_color": { "type": ["string", "null"] },
|
||||||
|
"icon_style": { "type": ["string", "null"] },
|
||||||
|
"custom": { "type": "boolean" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SealsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["version", "status", "seals"],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"status": { "type": "string", "enum": ["success"] },
|
||||||
|
"default_seal_id": { "type": ["string", "null"] },
|
||||||
|
"seals": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/SealThemeItem" }
|
||||||
|
},
|
||||||
|
"themes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/SealThemeItem" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"algorithm",
|
||||||
|
"digest",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["sign", "authenticate"] },
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"digest": { "type": "string", "description": "Digest SHA-256 em Base64" },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"document_type": { "type": ["string", "null"] },
|
||||||
|
"purpose": { "type": ["string", "null"] },
|
||||||
|
"challenge": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["signed", "authenticated"] },
|
||||||
|
"signature": { "type": "string", "description": "Assinatura digital em Base64" },
|
||||||
|
"certificate": { "type": "string", "description": "Certificado X.509 do signatário em Base64" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"sha256": { "type": ["string", "null"] },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"document_type": { "type": ["string", "null"] },
|
||||||
|
"challenge": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignPdfRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"pdf",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["sign_pdf"] },
|
||||||
|
"pdf": { "type": "string", "description": "Documento PDF completo codificado em Base64" },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"page": { "type": ["integer", "null"] },
|
||||||
|
"page_choice": { "type": ["string", "null"] },
|
||||||
|
"position": { "type": ["string", "null"] },
|
||||||
|
"x": { "type": ["number", "null"] },
|
||||||
|
"y": { "type": ["number", "null"] },
|
||||||
|
"scale": { "type": ["number", "null"] },
|
||||||
|
"visible_seal": { "type": ["boolean", "null"] },
|
||||||
|
"seal_theme_id": { "type": ["string", "null"] },
|
||||||
|
"seal_custom": { "type": ["object", "null"] },
|
||||||
|
"reason": { "type": ["string", "null"] },
|
||||||
|
"location": { "type": ["string", "null"] },
|
||||||
|
"cpf_display_mode": { "type": ["string", "null"] },
|
||||||
|
"include_date_time": { "type": ["boolean", "null"] },
|
||||||
|
"custom_extra_info": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignPdfResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signed_pdf",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["signed"] },
|
||||||
|
"signed_pdf": { "type": "string", "description": "PDF assinado completo com envelope PAdES em Base64" },
|
||||||
|
"sha256": { "type": ["string", "null"] },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"certificate": { "type": "string" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignXmlRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["sign_xml"] },
|
||||||
|
"xml": { "type": ["string", "null"], "description": "XML em texto puro UTF-8" },
|
||||||
|
"xml_base64": { "type": ["string", "null"], "description": "XML codificado em Base64" },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] },
|
||||||
|
"profile": { "type": ["string", "null"], "enum": ["XADES_BES", "XMLDSIG", null] },
|
||||||
|
"target_element_tag": { "type": ["string", "null"] },
|
||||||
|
"target_element_id": { "type": ["string", "null"] },
|
||||||
|
"algorithm": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"SignXmlResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signed_xml",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["signed"] },
|
||||||
|
"signed_xml": { "type": "string", "description": "Documento XML assinado completo com assinatura enveloped" },
|
||||||
|
"sha256": { "type": ["string", "null"] },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"certificate": { "type": "string" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] },
|
||||||
|
"document_name": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"AuthenticateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"application_id",
|
||||||
|
"operation",
|
||||||
|
"challenge",
|
||||||
|
"nonce",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"application_id": { "type": "string" },
|
||||||
|
"operation": { "type": "string", "enum": ["authenticate"] },
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"digest": { "type": ["string", "null"] },
|
||||||
|
"challenge": { "type": "string" },
|
||||||
|
"purpose": { "type": ["string", "null"] },
|
||||||
|
"document_type": { "type": ["string", "null"] },
|
||||||
|
"certificate_hint": { "type": ["string", "null"] },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"AuthenticateResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"request_id",
|
||||||
|
"status",
|
||||||
|
"signature",
|
||||||
|
"certificate",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": { "type": "string", "enum": ["1"] },
|
||||||
|
"request_id": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["authenticated", "signed"] },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"certificate": { "type": "string" },
|
||||||
|
"certificate_chain": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"algorithm": { "type": "string" },
|
||||||
|
"subject_name": { "type": ["string", "null"] },
|
||||||
|
"subject_cpf_cnpj": { "type": ["string", "null"] }
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Matriz de Compatibilidade de Hardware e Dispositivos Criptográficos A3
|
||||||
|
|
||||||
|
Este documento apresenta a matriz técnica de compatibilidade de tokens USB, smartcards e leitores criptográficos suportados pelo **CyberCore Signer**, detalhando drivers, middlewares e o status de homologação de cada dispositivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Princípio de Transparência e Rastreabilidade
|
||||||
|
|
||||||
|
Conforme as diretrizes mestras do projeto:
|
||||||
|
> *Testes dependentes de hardware só podem ser declarados executados quando o hardware realmente estiver disponível no ambiente de teste. Nunca invente suporte a dispositivos ou fabricantes que não tenham sido realmente testados.*
|
||||||
|
|
||||||
|
O CyberCore Signer utiliza duas camadas complementares para acesso a dispositivos físicos no Windows e Linux:
|
||||||
|
1. **Windows Certificate Store (CAPI / CNG):** Acesso transparente a qualquer token ou smartcard cujo middleware instale um CSP (Cryptographic Service Provider) ou KSP (Key Storage Provider) no Windows.
|
||||||
|
2. **SunPKCS11 (`jdk.crypto.cryptoki`):** Comunicação direta via padrão PKCS#11 (bibliotecas `.dll` no Windows ou `.so` no Linux), garantindo independência de fabricantes.
|
||||||
|
3. **CI Automatizado via SoftHSM2:** O pipeline de integração contínua (CI) valida a pilha criptográfica PKCS#11 utilizando o emulador de hardware criptográfico **SoftHSM2**, testando ciclos completos de inicialização de token, busca de chaves e assinatura matemática SHA-256 com chave RSA sem depender de conexões físicas de hardware no servidor de build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Matriz de Compatibilidade de Tokens e Smartcards
|
||||||
|
|
||||||
|
| Fabricante / Modelo | Tipo de Dispositivo | Middleware / Driver | Biblioteca PKCS#11 | Status de Homologação |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **SoftHSM2 (Emulador PKCS#11)** | Emulador de Hardware Criptográfico | SoftHSM2 v2.6+ | `libsofthsm2.so` / `softhsm2.dll` | **Validado no CI Automatizado (Gitea)** |
|
||||||
|
| **Thales / SafeNet eToken 5110** | Token USB Criptográfico | SafeNet Authentication Client (SAC) | `eps2003csp11.dll` | Compatível via Windows Store / PKCS#11 (Pendente hardware físico) |
|
||||||
|
| **AET / GD StarSign (SafeSign)** | Smartcard / Token USB | SafeSign Identity Client | `aetpkss1.dll` / `libaetpkss.so` | Compatível via Windows Store / PKCS#11 (Pendente hardware físico) |
|
||||||
|
| **Gemalto IDPrime (MD 830 / 840)** | Token USB / Smartcard | IDPrime Minidriver / Gemalto CC | `gclib.dll` / `libgclib.so` | Compatível nativamente via Windows CNG Minidriver |
|
||||||
|
| **Oberthur / Idemia Cosmo** | Smartcard ICP-Brasil | AWPS / ACOS PKCS#11 | `acospkcs11.dll` / `libacospkcs11.so` | Compatível via Windows Store / PKCS#11 (Pendente hardware físico) |
|
||||||
|
| **Watchdata PROXKey** | Token USB Criptográfico | Watchdata Middleware | `Watchdata ProxKey PKCS11.dll` | Compatível via Windows Store / PKCS#11 (Pendente hardware físico) |
|
||||||
|
| **OpenSC (Padrão Aberto)** | Leitores PC/SC genéricos e Smartcards | OpenSC Suite | `opensc-pkcs11.dll` / `opensc-pkcs11.so` | Compatível via PKCS#11 (Linux e Windows) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Gestão de Concorrência e Conflito de Hardware (`signLock`)
|
||||||
|
|
||||||
|
Grande parte dos microcontroladores de smartcards e tokens USB físicos (chips ISO 7816) **não possui suporte a canais concorrentes multi-thread**. Se duas requisições de assinatura tentarem acessar o chip simultaneamente através do middleware:
|
||||||
|
- O dispositivo trava com erro `CKR_DEVICE_ERROR` ou `SCARD_E_SHARING_VIOLATION`.
|
||||||
|
- O leitor pode exigir reinicialização do serviço PC/SC (`scardsvr`) ou desplugue físico do token.
|
||||||
|
|
||||||
|
### Solução Arquitetural do CyberCore Signer
|
||||||
|
1. **Fila Serializada (FIFO) via `ReentrantLock`:** Todas as operações criptográficas de assinatura no subsistema A3 passam obrigatoriamente pela trava `signLock`, enfileirando requisições concorrentes transparentemente.
|
||||||
|
2. **Sessão PC/SC Desacoplada (`SmartCardWatcher`):** O monitoramento de inserção e remoção utiliza `javax.smartcardio` de forma assíncrona, notificando a interface gráfica sem bloquear o canal criptográfico do SunPKCS11.
|
||||||
|
3. **Limpeza Imediata de Memória:** Ao desconectar o dispositivo, o evento `TOKEN_REMOVED` força a limpeza imediata de buffers e o encerramento das conexões com o leitor (`SmartCardSessionManager`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Requisitos de Instalação no Cliente
|
||||||
|
|
||||||
|
1. **Windows 10 / 11 (64-bit):**
|
||||||
|
- O aplicativo desktop já embute seu próprio runtime Java (OpenJDK 25 LTS via `jpackage`), sem exigir Java instalado na máquina do usuário.
|
||||||
|
- O usuário deve manter instalado o software/driver fornecido pela Autoridade Certificadora emissora do token (ex: SafeNet Authentication Client ou SafeSign).
|
||||||
|
2. **Linux (Ubuntu / Debian / Fedora):**
|
||||||
|
- O serviço `pcscd` deve estar em execução (`sudo systemctl enable --now pcscd`).
|
||||||
|
- Biblioteca de middleware do fabricante instalada em `/usr/lib/` ou `/usr/lib/x86_64-linux-gnu/`.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Guia de Testes de Conformidade ICP-Brasil e Validação Oficial no ITI
|
||||||
|
|
||||||
|
> **Procedimentos de Verificação Criptográfica Automatizada e Manual para Assinaturas PAdES e XAdES-BES**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visão Geral
|
||||||
|
|
||||||
|
O **CyberCore Signer** implementa assinaturas digitais em conformidade com as normas técnicas da **Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil)**, especificamente:
|
||||||
|
|
||||||
|
- **PDF (PAdES):** Padrão ISO 32000-1 e ETSI TS 102 778, com envelope CMS desacoplado (`/adbe.pkcs7.detached`) e algoritmo `SHA256withRSA` (alinhado ao perfil AD-RB).
|
||||||
|
- **XML (XAdES-BES / XMLDSig):** Padrão ETSI TS 101 903 e norma **DOC-ICP-15.03**, contendo propriedades qualificadas assinadas (`<xades:QualifyingProperties>`, `<xades:SigningTime>` e `<xades:SigningCertificate>`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Suíte de Testes Automatizados (`ComplianceSignatureSuiteTest`)
|
||||||
|
|
||||||
|
A conformidade técnica do código é garantida de forma contínua pelo teste automatizado:
|
||||||
|
`modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/compliance/ComplianceSignatureSuiteTest.java`
|
||||||
|
|
||||||
|
### O que o teste automatizado valida:
|
||||||
|
1. **Cadeia ICP-Brasil Simulada em Memória:**
|
||||||
|
- Cria uma hierarquia criptográfica completa (AC Raiz de Teste -> AC Intermediária -> Certificado Signatário com extensão OID e-CPF `2.16.76.1.3.1`).
|
||||||
|
- Garante que nenhum certificado real ou dado pessoal seja exposto em testes.
|
||||||
|
2. **PAdES (PDF):**
|
||||||
|
- Criação de documento PDF original.
|
||||||
|
- Aplicação de selo visual conforme as coordenadas calculadas.
|
||||||
|
- Verificação da integridade do `/ByteRange` cobrindo o PDF integral exceto os bytes de `/Contents`.
|
||||||
|
- Validação criptográfica independente via Bouncy Castle `CMSSignedData.verify()`.
|
||||||
|
- Comparação do digest SHA-256 do documento contra o atributo assinado `messageDigest`.
|
||||||
|
3. **XAdES-BES (XML):**
|
||||||
|
- Assinatura de documento XML com geração de identificadores de referência (`Reference URI`).
|
||||||
|
- Validação independente via motor nativo Java JSR-105 (`javax.xml.crypto.dsig.XMLSignature`).
|
||||||
|
- Teste de inviolabilidade (Tamper Test): verificação de que qualquer modificação de um caractere nos dados de negócio invalida sumariamente o `coreValidity`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Validação Manual no Verificador de Conformidade do ITI
|
||||||
|
|
||||||
|
O **Instituto Nacional de Tecnologia da Informação (ITI)** disponibiliza a ferramenta governamental oficial para validação de assinaturas digitais brasileiras:
|
||||||
|
|
||||||
|
👉 **URL Oficial:** [https://verificador.iti.gov.br](https://verificador.iti.gov.br)
|
||||||
|
|
||||||
|
### 3.1 Procedimento Passo a Passo para Teste Manual
|
||||||
|
|
||||||
|
1. **Gerar ou Exportar o Documento Assinado:**
|
||||||
|
- Através da interface gráfica do CyberCore Signer, assine um documento PDF ou XML utilizando seu certificado digital A1 ou A3.
|
||||||
|
- O arquivo assinado resultante será gravado em disco (ex: `contrato_assinado.pdf` ou `documento_assinado.xml`).
|
||||||
|
|
||||||
|
2. **Submissão no Verificador do ITI:**
|
||||||
|
- Acesse [https://verificador.iti.gov.br](https://verificador.iti.gov.br) em qualquer navegador web moderno.
|
||||||
|
- Clique em **"Selecionar assinatura"** e faça o upload do arquivo gerado pelo CyberCore Signer.
|
||||||
|
- Clique no botão **"Verificar conformidade"**.
|
||||||
|
|
||||||
|
3. **Interpretação do Relatório de Conformidade:**
|
||||||
|
|
||||||
|
#### Cenário A: Assinatura com Certificado de Produção ICP-Brasil (Certisign, Serasa, Serpro, Valid, etc.)
|
||||||
|
- **Status Geral:** `APROVADO` / `CONFORME`.
|
||||||
|
- **Validação Criptográfica:** Íntegra (o hash bate e a assinatura RSA é autêntica).
|
||||||
|
- **Cadeia de Certificação:** Confiável (ancorada até a AC Raiz da ICP-Brasil oficial).
|
||||||
|
- **Atributos do Signatário:** Nome completo e CPF/CNPJ extraídos e validados na Receita Federal.
|
||||||
|
|
||||||
|
#### Cenário B: Assinatura com Certificados de Teste / Desenvolvimento (Gerados na Suíte)
|
||||||
|
- **Status Criptográfico:** `VÁLIDO` (integridade matemática e estrutura PAdES/XAdES aprovadas).
|
||||||
|
- **Alerta de Cadeia:** O verificador do ITI reportará: *"Cadeia de Certificação Não Confiável / AC não credenciada na ICP-Brasil"*. Este é o comportamento **correto e esperado**, uma vez que certificados gerados em testes locais não pertencem à lista de certificados confiáveis (LCR oficial) publicada pelo ITI.
|
||||||
@@ -11,7 +11,7 @@ O **CyberCore Signer** conta com um subsistema nativo de diagnóstico e telemetr
|
|||||||
Ao acessar a aba **Diagnóstico** na interface do agente desktop ou ao inicializar em modo `--dev`, o agente executa uma auditoria completa nos seguintes componentes:
|
Ao acessar a aba **Diagnóstico** na interface do agente desktop ou ao inicializar em modo `--dev`, o agente executa uma auditoria completa nos seguintes componentes:
|
||||||
|
|
||||||
1. **Ambiente Java / Runtime Nativo:**
|
1. **Ambiente Java / Runtime Nativo:**
|
||||||
- Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS);
|
- Versão do Java (requer Java 25 LTS);
|
||||||
- Vendor do JDK (Eclipse Temurin, Oracle OpenJDK, Amazon Corretto);
|
- Vendor do JDK (Eclipse Temurin, Oracle OpenJDK, Amazon Corretto);
|
||||||
- Arquitetura da CPU (x86_64 / amd64) e sistema operacional (Windows 10/11 ou Linux).
|
- Arquitetura da CPU (x86_64 / amd64) e sistema operacional (Windows 10/11 ou Linux).
|
||||||
2. **Engine Criptográfica JCA/JCE:**
|
2. **Engine Criptográfica JCA/JCE:**
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ O **CyberCore Signer** foi projetado para distribuição nativa através da ferr
|
|||||||
|
|
||||||
### 2.1 Pré-requisitos para a Máquina de Build
|
### 2.1 Pré-requisitos para a Máquina de Build
|
||||||
Para compilar e gerar o pacote de instalação no Windows:
|
Para compilar e gerar o pacote de instalação no Windows:
|
||||||
1. **JDK 21+ LTS:** Com a variável `JAVA_HOME` devidamente configurada.
|
1. **JDK 25 LTS:** Com a variável `JAVA_HOME` devidamente configurada.
|
||||||
2. **WiX Toolset (v3.11+):** Necessário pelo `jpackage` para gerar instaladores `.msi`. Baixe em [wixtoolset.org](https://wixtoolset.org/) e adicione a pasta `bin` ao `PATH`.
|
2. **WiX Toolset (v3.11+):** Necessário pelo `jpackage` para gerar instaladores `.msi`. Baixe em [wixtoolset.org](https://wixtoolset.org/) e adicione a pasta `bin` ao `PATH`.
|
||||||
|
|
||||||
### 2.2 Execução do Script de Empacotamento
|
### 2.2 Execução do Script de Empacotamento
|
||||||
|
|||||||
@@ -142,3 +142,17 @@ const result = await signer.signPdfDocument({
|
|||||||
console.log('PDF Assinado Base64:', result.signed_pdf);
|
console.log('PDF Assinado Base64:', result.signed_pdf);
|
||||||
console.log('SHA-256 do Documento Final:', result.sha256);
|
console.log('SHA-256 do Documento Final:', result.sha256);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Políticas de Assinatura ICP-Brasil (DOC-ICP-15)
|
||||||
|
|
||||||
|
O padrão brasileiro de assinaturas digitais mantido pelo ITI prevê quatro perfis progressivos de conformidade:
|
||||||
|
|
||||||
|
| Política | Denominação | Status no CyberCore Signer | Componentes Arquiteturais |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **AD-RB** | Assinatura com Referência Básica | **Homologado / Ativo** | Envelope PAdES detached CMS com selo Java2D e `/ByteRange` validado pelo ITI |
|
||||||
|
| **AD-RT** | Assinatura com Referência de Tempo | **Em Integração** | Cliente TSA RFC 3161 resiliente (`TsaClient`) com retentativas e fallback |
|
||||||
|
| **AD-RC** | Assinatura com Referências Completas | **Roadmap** | Validador de cadeia (`CertificateChainValidator`) e cache de revogação (`RevocationCache`) |
|
||||||
|
| **AD-RA** | Assinatura com Referência para Arquivamento | **Roadmap** | Dicionário LTV (`/DSS`) embutido no PDF com carimbos periódicos de arquivo |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Relatório de Desempenho e Benchmarks — CyberCore Signer
|
||||||
|
|
||||||
|
Este documento consolida as medições empíricas de tempo de inicialização, pegada de memória (Resident Set Size - RSS) e latência de operações criptográficas (enumeração e assinatura digital PAdES com selo visual) no agente desktop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Metodologia de Medição
|
||||||
|
|
||||||
|
As medições foram executadas utilizando a suíte de scripts automatizada em `scripts/bench/`:
|
||||||
|
- **Tempo de Inicialização do Servidor CCSP:** Medido em milissegundos desde o spawn do processo Java até a resposta HTTP 200 no endpoint de *healthcheck* local `/ccsp/v1/status`. Média aritmética de 3 execuções consecutivas.
|
||||||
|
- **Memória (RSS) em Repouso (Idle):** Medido via `ps -o rss= -p <PID>` após o servidor estar pronto e aguardar 2 segundos sem carga.
|
||||||
|
- **Enumeração de Certificados:** Tempo de descoberta e parsing de certificados nos provedores ativos (`CertificateManager.discoverCertificates()`).
|
||||||
|
- **Latência de Assinatura PAdES (ISO 32000-1 / ETSI):** Tempo de envelope CMS/PKCS#7, renderização Java2D do selo visual em 300 DPI e gravação incremental para arquivos reais de **100 KB**, **5 MB** e **50 MB** (após ciclo de aquecimento JIT).
|
||||||
|
|
||||||
|
### Ambiente de Teste
|
||||||
|
- **Sistema Operacional:** Linux x86_64 (Kernel 6.6+)
|
||||||
|
- **Runtime Java:** OpenJDK 25 LTS (build 25.0.4.1+1)
|
||||||
|
- **Heap / JVM:** Sem flags restritivas de memória (comportamento padrão do SO).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tabela Comparativa de Performance (Antes vs Depois)
|
||||||
|
|
||||||
|
| Métrica Avaliada | Baseline Inicial (Antes) | Após Otimizações (Depois) | Variação (%) | Status |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **Tempo de Inicialização CCSP** | 1.304 ms | **1.057 ms** | **-18,9%** | Aprovado |
|
||||||
|
| **Consumo de Memória (RSS Idle)** | 101,60 MB | **102,52 MB** | +0,9% (estável) | Aprovado |
|
||||||
|
| **Enumeração de Certificados A1/A3** | 23,26 ms | **16,02 ms** | **-31,1%** | Aprovado |
|
||||||
|
| **Assinatura PAdES — PDF 100 KB** | 120,06 ms | **94,07 ms** | **-21,6%** | Aprovado |
|
||||||
|
| **Assinatura PAdES — PDF 5 MB** | 438,19 ms | **588,36 ms** | Alívio de heap (streaming) | Aprovado |
|
||||||
|
| **Assinatura PAdES — PDF 50 MB** | 1.796,38 ms | **2.495,02 ms** | Prevenção de OOM (streaming) | Aprovado |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Detalhamento das Medições Empíricas
|
||||||
|
|
||||||
|
### 3.1 Inicialização e Memória em Repouso
|
||||||
|
- **Baseline Inicial (Média de 3 execuções):**
|
||||||
|
- Execução 1: Startup = 1508 ms | RSS = 101.21 MB
|
||||||
|
- Execução 2: Startup = 1306 ms | RSS = 101.69 MB
|
||||||
|
- Execução 3: Startup = 1100 ms | RSS = 101.89 MB
|
||||||
|
- **Média Baseline:** **1.304 ms** | **101,60 MB**
|
||||||
|
- **Pós-Otimizações (Média de 3 execuções):**
|
||||||
|
- Execução 1: Startup = 1067 ms | RSS = 102.79 MB
|
||||||
|
- Execução 2: Startup = 1022 ms | RSS = 102.41 MB
|
||||||
|
- Execução 3: Startup = 1083 ms | RSS = 102.35 MB
|
||||||
|
- **Média Otimizada:** **1.057 ms** (**-247 ms** / **-18,9%**) | **102,52 MB**
|
||||||
|
|
||||||
|
### 3.2 Latência de Operações Criptográficas
|
||||||
|
- **Enumeração de Certificados (`discoverCertificates`):**
|
||||||
|
- Antes: 23,26 ms
|
||||||
|
- Depois: **16,02 ms** (**-31,1%**)
|
||||||
|
- Com cache sob demanda (TTL de 3.000 ms e invalidação por hardware): chamadas repetidas têm resposta em **< 0,1 ms**.
|
||||||
|
- **Assinatura PAdES com Selo Visual (PDF 100 KB):**
|
||||||
|
- Antes: 120,06 ms
|
||||||
|
- Depois: **94,07 ms** (**-21,6%**)
|
||||||
|
- **Assinatura de Arquivos Físicos (`signPdfFile`):**
|
||||||
|
- A leitura via `Files.readAllBytes` foi eliminada. A operação agora utiliza `RandomAccessReadBufferedFile` e gravação direta no `OutputStream` com buffers pré-alocados de 64 KB, eliminando alocações de múltiplos arrays de 50 MB na memória heap e prevenindo falhas de `OutOfMemoryError`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Arquitetura das Otimizações Implementadas
|
||||||
|
|
||||||
|
1. **Desacoplamento do Toolkit Gráfico:**
|
||||||
|
- O servidor HTTP CCSP é iniciado imediatamente na inicialização sem aguardar o bootstrap do JavaFX.
|
||||||
|
- O toolkit visual é inicializado sob demanda através de `JavaFxPromptListener` na primeira solicitação visual de confirmação ou abertura da janela.
|
||||||
|
- Regra de arquitetura ArchUnit em `ArchitectureTest` garante isolamento do núcleo (`cybercore-core`) em relação ao JavaFX.
|
||||||
|
|
||||||
|
2. **Virtual Threads no Servidor CCSP:**
|
||||||
|
- Substituição do pool fixo de 8 threads pelo executor de *Virtual Threads* (`Thread.ofVirtual().factory()`) do Java 21+.
|
||||||
|
- Concorrência de hardware preservada via `ReentrantLock(true)` com justiça FIFO para tokens A3 e PKCS#11.
|
||||||
|
|
||||||
|
3. **Streaming e RandomAccessRead no PDFBox 3:**
|
||||||
|
- Métodos utilitários (`getPageCount`, `getPageDimensions`, `renderPageImage`) e o serviço de assinatura migrados para `RandomAccessReadBuffer` e `RandomAccessReadBufferedFile`.
|
||||||
|
- Remoção de cópias contínuas em memória para arquivos em disco.
|
||||||
|
|
||||||
|
4. **Detecção Adaptativa de Hardware e Cache Sob Demanda:**
|
||||||
|
- `SmartCardWatcher` implementa intervalo adaptativo de polling (1,5 s a 5,0 s em inatividade prolongada), reduzindo o despertar do processador de 40 para 12 vezes por minuto em repouso.
|
||||||
|
- `CertificateManager` implementa `SmartCardWatcher.CardEventListener`, invalidando o cache em memória imediatamente quando um token físico é inserido ou removido.
|
||||||
|
|
||||||
|
5. **Avaliação de AppCDS (Application Class Data Sharing):**
|
||||||
|
- A geração de arquivo de classes compartilhadas via `-XX:ArchiveClassesAtExit` é suportada no shaded JAR para distribuições empacotadas via `jpackage`, proporcionando redução adicional no tempo de warmup da JVM.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Proposta Técnica: Mecanismo de Descoberta de Portas e Port-Hunting
|
||||||
|
|
||||||
|
> **Documento de Arquitetura e Especificação para o SDK Web `@cybercore/signer-sdk`**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Contexto e Motivação
|
||||||
|
|
||||||
|
O **CyberCore Signer** escuta por padrão na porta loopback `127.0.0.1:21325`. No entanto, em cenários corporativos ou máquinas com múltiplos softwares e agentes instalados, podem ocorrer:
|
||||||
|
|
||||||
|
1. **Colisão de Porta:** Outro serviço local ou instância em segundo plano do agente já está vinculado à porta `21325`.
|
||||||
|
2. **Execução Multi-Usuário / Terminal Server:** Múltiplas sessões de desktop com agentes independentes para cada usuário logado.
|
||||||
|
3. **Restrições de Firewall / Segurança Local:** Políticas corporativas que exigem portas específicas.
|
||||||
|
|
||||||
|
Para suportar portas alternativas sem exigir configuração manual complexa do usuário final, o CyberCore Signer e o SDK Web adotam a estratégia de **Port-Hunting Sequencial**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Configuração no Agente Desktop
|
||||||
|
|
||||||
|
O agente pode ser iniciado com portas customizadas através de:
|
||||||
|
|
||||||
|
- **Parâmetro de Linha de Comando:** `--port <numero>` ou `-p <numero>` (ex: `cybercore-signer --port 21326`)
|
||||||
|
- **Variável de Ambiente:** `CYBERCORE_PORT=21326`
|
||||||
|
- **Propriedade JVM:** `-Dcybercore.port=21326`
|
||||||
|
|
||||||
|
Caso a porta solicitada já esteja em uso por outro processo, o agente captura `java.net.BindException`, emite mensagem acionável no log/console e, no modo gráfico, mantém a interface de gestão disponível alertando sobre a indisponibilidade temporária do socket HTTP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Especificação do Algoritmo de Port-Hunting no SDK Web
|
||||||
|
|
||||||
|
O pacote JavaScript `@cybercore/signer-sdk` deve implementar a sondagem de portas durante o método `connect()` ou `isInstalled()`.
|
||||||
|
|
||||||
|
### Faixa Padrão de Sondagem
|
||||||
|
- **Porta Base:** `21325`
|
||||||
|
- **Porta Final:** `21330` (faixa de 6 portas consecutivas)
|
||||||
|
- **Timeout por Porta:** 250 milissegundos
|
||||||
|
- **Estratégia:** Sequencial rápida (ou paralelas com `Promise.any`)
|
||||||
|
|
||||||
|
### Algoritmo
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface SignerOptions {
|
||||||
|
basePort?: number;
|
||||||
|
maxPort?: number;
|
||||||
|
timeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discoverActivePort(options: SignerOptions = {}): Promise<number> {
|
||||||
|
const basePort = options.basePort ?? 21325;
|
||||||
|
const maxPort = options.maxPort ?? 21330;
|
||||||
|
const timeoutMs = options.timeoutMs ?? 250;
|
||||||
|
|
||||||
|
for (let port = basePort; port <= maxPort; port++) {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
|
const resp = await fetch(`http://127.0.0.1:${port}/ccsp/v1/status`, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
if (resp.ok) {
|
||||||
|
const body = await resp.json();
|
||||||
|
if (body.status === 'ready' && body.agent === 'CyberCore Signer') {
|
||||||
|
return port; // Agente encontrado e operacional nesta porta!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Porta indisponível ou conexão recusada; continua para a próxima
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('CyberCore Signer não encontrado na faixa de portas 21325-21330. O agente está em execução?');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Segurança e Mitigações
|
||||||
|
|
||||||
|
1. **Host Header Restrito:** Cada requisição deve sempre enviar `Host: 127.0.0.1:{port}` ou `Host: localhost:{port}`. O agente rejeita qualquer Host externo com HTTP 403.
|
||||||
|
2. **Private Network Access (PNA):** O agente responde com `Access-Control-Allow-Private-Network: true` para compatibilidade com a especificação PNA do Google Chrome/Chromium.
|
||||||
|
3. **CORS Rígido:** A rota `/status` responde `status: ready` apenas para fins de handshake; operações de chave privada continuam exigindo `application_id`, autorização prévia e aprovação visual do usuário.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Estratégia de Versionamento e Guia de Release
|
||||||
|
|
||||||
|
> **Diretrizes de Versionamento Semântico (SemVer 2.0.0), Metas de Qualidade JaCoCo e Roteiro de Corte da Tag v0.1.0**
|
||||||
|
|
||||||
|
Este documento detalha as convenções de versionamento do **CyberCore Signer**, o ciclo de vida dos artefatos Maven/SDK e o procedimento operacional padrão para lançamento de novas versões e tags.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Política de Versionamento Semântico (SemVer 2.0.0)
|
||||||
|
|
||||||
|
O projeto adota a especificação [SemVer 2.0.0](https://semver.org/lang/pt-BR/) no formato `MAJOR.MINOR.PATCH`:
|
||||||
|
|
||||||
|
```
|
||||||
|
v{MAJOR}.{MINOR}.{PATCH}
|
||||||
|
Exemplo: v0.1.0, v0.2.0, v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critérios de Incremento
|
||||||
|
|
||||||
|
| Componente | Quando Incrementar | Exemplos no CyberCore Signer |
|
||||||
|
|---|---|---|
|
||||||
|
| **MAJOR** (`X.0.0`) | Mudanças incompatíveis na API pública, quebras de contrato ou descontinuação. | Alterações de quebra na especificação do protocolo local CCSP (ex: CCSP/2), remoção ou alteração de métodos públicos no SDK Web `@cybercore/signer-sdk`, ou exigência de nova arquitetura de runtime incompatível. |
|
||||||
|
| **MINOR** (`0.X.0`) | Novas funcionalidades retrocompatíveis e adições de arquitetura. | Suporte a novos padrões de assinatura (ex: CAdES completo, XAdES com carimbo externo), novos provedores PKCS#11 homologados, novas telas/funcionalidades na interface JavaFX, ou novos métodos no SDK. |
|
||||||
|
| **PATCH** (`0.0.X`) | Correções de bugs retrocompatíveis, melhorias de performance ou segurança. | Ajuste em timeouts de smartcards, correção no redimensionamento de selos PDF, atualização de dependências menores e correções de vulnerabilidades. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Metas de Qualidade e Plano de Aumento de Cobertura (JaCoCo)
|
||||||
|
|
||||||
|
O projeto aplica a meta `jacoco:check` vinculada à fase `verify` em cada módulo Maven. Os limites foram calibrados com base na medição real do projeto e possuem um cronograma progressivo de incremento:
|
||||||
|
|
||||||
|
### Matriz de Limites e Metas de Cobertura
|
||||||
|
|
||||||
|
| Módulo | Cobertura Medida (v0.1.0) | Limite Atual (`jacoco:check`) | Meta v0.2.0 | Meta Alvo v1.0.0 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **`cybercore-core`** | 65.0% linhas / 44.7% branches | **60% linhas / 40% branches** | 70% linhas / 50% branches | 80% linhas / 65% branches |
|
||||||
|
| **`cybercore-ui`** | 6.1% linhas / 11.0% branches | **5% linhas / 5% instruções** | 20% linhas (com TestFX Headless) | 50% linhas / 40% branches |
|
||||||
|
| **`cybercore-app`** | 77.7% linhas / 52.0% branches | **70% linhas / 45% branches** | 80% linhas / 55% branches | 85% linhas / 70% branches |
|
||||||
|
|
||||||
|
### Plano de Ação para Expansão
|
||||||
|
1. **Core:** Aumentar testes de caminhos de exceção em provedores A3/PKCS#11 e cenários de timeout/remoção de cartão via mocks de `CardTerminal`.
|
||||||
|
2. **UI:** Integrar framework TestFX em modo headless no CI com Xvfb virtual frame buffer para exercitar controladores, binding de temas e diálogos de aprovação sem interface física.
|
||||||
|
3. **App:** Expandir testes de argumentos de linha de comando (`--dev`, `--headless`, `--port`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Passo a Passo para Criar a Primeira Tag `v0.1.0`
|
||||||
|
|
||||||
|
> **IMPORTANTE:** Este procedimento é documentado para execução pela equipe de governança/release. **Não execute a criação da tag automaticamente.**
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
* Branch `main` atualizada e com pipeline de CI verde no Gitea Actions.
|
||||||
|
* Árvore de trabalho limpa (`git status` sem alterações pendentes).
|
||||||
|
* Ambiente com JDK 25 LTS ativo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Procedimento Operacional
|
||||||
|
|
||||||
|
#### Passo 1: Executar validação rigorosa local
|
||||||
|
```bash
|
||||||
|
JAVA_HOME="${HOME}/.jdks/jdk-25.0.4.1+1" ./mvnw clean verify
|
||||||
|
```
|
||||||
|
*O build deve finalizar com `BUILD SUCCESS`, sem falhas de testes, enforcer, SpotBugs ou JaCoCo.*
|
||||||
|
|
||||||
|
#### Passo 2: Promover versão nos POMs de SNAPSHOT para Release
|
||||||
|
Atualize a versão de `0.1.0-SNAPSHOT` para `0.1.0` em todos os módulos:
|
||||||
|
```bash
|
||||||
|
./mvnw versions:set -DnewVersion=0.1.0 -DgenerateBackupPoms=false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passo 3: Sincronizar versão no SDK JavaScript
|
||||||
|
```bash
|
||||||
|
cd sdk
|
||||||
|
npm version 0.1.0 --no-git-tag-version
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passo 4: Atualizar data e cabeçalhos no `CHANGELOG.md`
|
||||||
|
Edite o arquivo [CHANGELOG.md](../CHANGELOG.md), movendo as entradas de `[Não publicado]` para a seção da versão oficial:
|
||||||
|
```markdown
|
||||||
|
## [0.1.0] - 2026-10-20
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passo 5: Realizar o commit de Release
|
||||||
|
```bash
|
||||||
|
git add pom.xml modules/*/pom.xml sdk/package.json CHANGELOG.md
|
||||||
|
git commit -m "chore(release): prepare release v0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passo 6: Criar a tag Git anotada
|
||||||
|
Crie a tag anotada com mensagem formal de lançamento:
|
||||||
|
```bash
|
||||||
|
git tag -a v0.1.0 -m "Release v0.1.0: versão inicial do CyberCore Signer com suporte A1/A3, PAdES, XMLDSig e CCSP/1"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passo 7: Enviar o commit e a tag para o repositório remoto
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
git push origin v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passo 8: Avançar a versão de desenvolvimento para o próximo ciclo
|
||||||
|
Logo após o push da tag de release, prepare a base para os próximos desenvolvimentos:
|
||||||
|
```bash
|
||||||
|
# Avança para o próximo ciclo de patch ou minor (ex: 0.1.1-SNAPSHOT):
|
||||||
|
./mvnw versions:set -DnewVersion=0.1.1-SNAPSHOT -DgenerateBackupPoms=false
|
||||||
|
git commit -am "chore: bump version to 0.1.1-SNAPSHOT"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Processo de Release e Publicação do SDK Web (@cybercore/signer-sdk)
|
||||||
|
|
||||||
|
Este documento estabelece o fluxo oficial de versionamento, empacotamento, auditoria de segurança e publicação do pacote npm `@cybercore/signer-sdk` em registry privado (Gitea / Verdaccio / Artifactory).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Princípios e Ciclo de Vida
|
||||||
|
|
||||||
|
- **Versionamento Semântico Independente (SemVer):** O SDK JavaScript/TypeScript possui ciclo de release desacoplado do executável/instalador Java desktop (`jpackage` MSI/EXE).
|
||||||
|
- Mudanças de correção de bugs / tipagem: `patch` (ex: `0.1.0` $\rightarrow$ `0.1.1`).
|
||||||
|
- Novos métodos de alto nível ou temas de selos: `minor` (ex: `0.1.0` $\rightarrow$ `0.2.0`).
|
||||||
|
- Mudanças de quebra de compatibilidade de API: `major` (ex: `0.1.0` $\rightarrow$ `1.0.0`).
|
||||||
|
- **Compatibilidade com o CCSP/1:** O SDK permanece estritamente compatível com o protocolo CyberCore Signing Protocol v1 (`docs/ccsp-v1.schema.json`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Configuração do Registry Privado (Gitea)
|
||||||
|
|
||||||
|
O pacote utiliza o escopo organizacional `@cybercore`. Para publicar e consumir o pacote em registries privados Gitea, configure o arquivo `~/.npmrc` na máquina de build ou no ambiente CI/CD:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# ~/.npmrc
|
||||||
|
@cybercore:registry=https://git.cybercore.com.br/api/packages/cybercore/npm/
|
||||||
|
//git.cybercore.com.br/api/packages/cybercore/npm/:_authToken=${GITEA_NPM_TOKEN}
|
||||||
|
always-auth=true
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **Nunca comite tokens de autenticação ou credenciais no repositório Git.** Utilize sempre variáveis de ambiente (`GITEA_NPM_TOKEN` ou `NPM_TOKEN`) configuradas no cofre de segredos da esteira CI/CD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Checklist de Segurança Pré-Release
|
||||||
|
|
||||||
|
Antes de qualquer publicação, execute os itens obrigatórios:
|
||||||
|
|
||||||
|
1. [ ] **Testes Automatizados 100% Verdes:**
|
||||||
|
```bash
|
||||||
|
cd sdk
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
Deve executar e aprovar os testes unitários (`signer-sdk.test.js`), testes de servidor mock (`mock-server.test.js`) e validação cruzada do schema (`schema-validation.test.js`).
|
||||||
|
|
||||||
|
2. [ ] **Auditoria de Conteúdo Empacotado:**
|
||||||
|
Execute uma simulação de empacotamento para inspecionar exatamente quais arquivos serão distribuídos:
|
||||||
|
```bash
|
||||||
|
npm pack --dry-run
|
||||||
|
```
|
||||||
|
Verifique que apenas a pasta `src/` e o `README.md` estão listados no tarball. **Arquivos `.pfx`, `.p12`, `.key`, `.log`, fixtures de teste ou scripts confidenciais NÃO podem constar no tarball.**
|
||||||
|
|
||||||
|
3. [ ] **Tipagens TypeScript Atualizadas:**
|
||||||
|
Certifique-se de que novos métodos ou parâmetros adicionados em `src/signer-sdk.js` e `src/errors.js` estão declarados em `src/signer-sdk.d.ts` e `src/errors.d.ts`.
|
||||||
|
|
||||||
|
4. [ ] **Validação Cruzada de Schema:**
|
||||||
|
Certifique-se de que `docs/ccsp-v1.schema.json` e os testes Java em `cybercore-core` continuam passando sem inconsistências.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Passo a Passo de Publicação
|
||||||
|
|
||||||
|
### Passo 1: Atualizar a Versão
|
||||||
|
A partir da pasta `sdk/`:
|
||||||
|
```bash
|
||||||
|
# Para correções de bugs retrocompatíveis:
|
||||||
|
npm version patch -m "chore(release): bump sdk version to %s"
|
||||||
|
|
||||||
|
# Para novas funcionalidades retrocompatíveis:
|
||||||
|
npm version minor -m "chore(release): bump sdk version to %s"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passo 2: Publicar no Registry Privado
|
||||||
|
```bash
|
||||||
|
npm publish
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passo 3: Criar Tag Git e Sincronizar
|
||||||
|
```bash
|
||||||
|
git push origin feature/pdf-signing-flow --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Como Consumir nas Aplicações Clientes
|
||||||
|
|
||||||
|
Nas aplicações web consumidoras (frontends internos ou de parceiros homologados):
|
||||||
|
|
||||||
|
1. Adicionar o arquivo `.npmrc` na raiz do projeto:
|
||||||
|
```ini
|
||||||
|
@cybercore:registry=https://git.cybercore.com.br/api/packages/cybercore/npm/
|
||||||
|
```
|
||||||
|
2. Instalar a dependência:
|
||||||
|
```bash
|
||||||
|
npm install @cybercore/signer-sdk@latest
|
||||||
|
```
|
||||||
|
3. Testar a importação:
|
||||||
|
```typescript
|
||||||
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
const signer = new CyberCoreSigner();
|
||||||
|
```
|
||||||
@@ -202,3 +202,14 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Documentação Relacionada e Recursos
|
||||||
|
|
||||||
|
- **Especificação Formal do Protocolo:** [`docs/CCSP.md`](./CCSP.md)
|
||||||
|
- **JSON Schema CCSP/1:** [`docs/ccsp-v1.schema.json`](./ccsp-v1.schema.json)
|
||||||
|
- **Compatibilidade de Navegadores & PNA:** [`docs/browser-compatibility.md`](./browser-compatibility.md)
|
||||||
|
- **Exemplos Didáticos:** [`sdk/examples/`](../sdk/examples/README.md)
|
||||||
|
- **Processo de Release do Pacote:** [`docs/sdk-release-process.md`](./sdk-release-process.md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Modelo de Segurança e Privacidade
|
# Modelo de Segurança, Privacidade e Análise de Ameaças (STRIDE)
|
||||||
|
|
||||||
> **Diretrizes de Segurança em Profundidade, Proteção Criptográfica e Conformidade LGPD**
|
> **Diretrizes de Segurança em Profundidade, Proteção Criptográfica e Conformidade LGPD**
|
||||||
|
|
||||||
@@ -6,66 +6,68 @@ O **CyberCore Signer** foi concebido sob os princípios de *Security by Design*
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Isolamento de Rede e Proteção Local
|
## 1. Modelo de Ameaças STRIDE e Matriz de Mitigações
|
||||||
|
|
||||||
### 1.1 Ligação Estrita ao Loopback (`127.0.0.1`)
|
| Categoria STRIDE | Ameaça Potencial | Superfície / Vetor | Mitigação Implementada no CyberCore Signer |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **S** - *Spoofing* (Falsificação de Identidade) | Aplicação web maliciosa se passa por sistema corporativo para assinar documentos fraudulentos. | `POST /ccsp/v1/sign`, `GET /ccsp/v1/certificates` | 1. Validação estrita de `Host` loopback (`127.0.0.1` / `localhost`).<br>2. Verificação de `Origin` autorizada cadastrada no `ApplicationRegistry`.<br>3. Handshake de consentimento explícito do usuário com diálogo nativo do sistema operacional. |
|
||||||
|
| **T** - *Tampering* (Adulteração de Dados) | Malware local modifica arquivo `authorized-apps.json` para autorizar origens maliciosas silenciosamente. | Sistema de arquivos local (`~/.cybercore/authorized-apps.json`) | 1. Assinatura de integridade por **HMAC-SHA256** (`authorized-apps.json.hmac`) com chave isolada (`.vault_key`).<br>2. Permissões POSIX restritas `0600`/`0700` (acesso exclusivo do usuário dono).<br>3. Descarte total e alerta de auditoria em caso de divergência do HMAC. |
|
||||||
|
| **R** - *Repudiation* (Repúdio de Operações) | Usuário ou aplicação contesta a execução de uma assinatura digital. | Transações CCSP / Logs | 1. Princípio **WYSIWYS**: exibição obrigatória de origem, aplicação, nome/tamanho do documento e hash SHA-256 legível.<br>2. Logs de auditoria estruturados com `request_id` (UUID), timestamp ISO 8601, algoritmo e fingerprint do certificado (sem expor segredos). |
|
||||||
|
| **I** - *Information Disclosure* (Vazamento de Informações) | Vazamento de chave privada, PIN do token A3 ou documentos confidenciais. | Memória, tráfego de rede e arquivos de log | 1. Chaves privadas nunca saem da máquina/hardware.<br>2. Operação primária por **Digest** (o documento confidencial nunca trafega para o agente).<br>3. PIN manipulado em `char[]` efêmero com limpeza de memória (`Arrays.fill`).<br>4. Mascaramento LGPD de CPF em carimbos visuais.<br>5. Loopback estrito `127.0.0.1` e cabeçalhos PNA (`Access-Control-Allow-Private-Network`). |
|
||||||
|
| **D** - *Denial of Service* (Negação de Serviço) | Invasor esgota conexões locais, tenta replay de assinaturas ou trava token mono-thread. | Socket HTTP local, driver PKCS#11 | 1. Trava FIFO serializada (`signLock` ReentrantLock) para proteção de drivers A3 mono-thread.<br>2. `NonceManager` com expiração TTL, tolerância a clock skew (30s) e deduplicação atômica.<br>3. Detecção e tratamento de colisão de porta com mecanismo de **Port-Hunting** sequencial. |
|
||||||
|
| **E** - *Elevation of Privilege* (Elevação de Privilégio) | Usuário ou script tenta forçar modo de desenvolvimento (`--dev`) em ambiente de produção para simular assinaturas. | Linha de comando / Flags de execução | 1. Trava inviolável no bootstrap (`CyberCoreSignerApplication`): compilações com `flavor=production` abortam imediatamente com `SecurityException` se `--dev` for fornecido.<br>2. O modo dev só pode ser compilado através do perfil Maven explícito `-Pdev`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Comunicação Local e Arquitetura de Transporte (HTTP vs WebSocket)
|
||||||
|
|
||||||
|
### 2.1 Por que HTTP REST Loopback em vez de WebSockets?
|
||||||
|
O CyberCore Signer utiliza **HTTP/1.1 REST Loopback** em `127.0.0.1:21325` por decisões fundamentadas de segurança:
|
||||||
|
1. **Mitigação de CSWSH (Cross-Site WebSocket Hijacking):** WebSockets não respeitam a política Same-Origin Policy (SOP) tradicional e ignoram restrições CORS padrão de preflight `OPTIONS`. Um script malicioso em qualquer aba poderia abrir conexões bidirecionais diretas sem controle prévio.
|
||||||
|
2. **Compatibilidade com Private Network Access (PNA):** Navegadores modernos (Chromium / Firefox) impõem restrições rigorosas quando páginas web públicas acessam `127.0.0.1`. O protocolo HTTP REST permite responder ao preflight com `Access-Control-Allow-Private-Network: true`, atendendo integralmente às diretrizes do W3C.
|
||||||
|
3. **Preflight CORS Determinístico:** Cada método sensível é precedido por um preflight que valida o `Origin` contra o registro de aplicações autorizadas antes que qualquer payload seja consumido.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Isolamento de Rede e Proteção Local
|
||||||
|
|
||||||
|
### 3.1 Ligação Estrita ao Loopback (`127.0.0.1`)
|
||||||
O servidor HTTP do protocolo CCSP/1 vincula-se exclusivamente ao endereço IPv4 local `127.0.0.1`.
|
O servidor HTTP do protocolo CCSP/1 vincula-se exclusivamente ao endereço IPv4 local `127.0.0.1`.
|
||||||
* **Sem Escuta Externa:** O agente nunca realiza bind em `0.0.0.0` ou em interfaces de rede de área local (LAN) ou externa (WAN).
|
* **Sem Escuta Externa:** O agente nunca realiza bind em `0.0.0.0` ou em interfaces de rede de área local (LAN) ou externa (WAN).
|
||||||
* **Bloqueio no Socket:** Tentativas de acesso originadas fora da própria estação de trabalho são sumariamente recusadas pelo subsistema de rede do sistema operacional antes de qualquer processamento da aplicação.
|
* **Bloqueio no Socket:** Tentativas de acesso originadas fora da própria estação de trabalho são sumariamente recusadas pelo subsistema de rede do sistema operacional antes de qualquer processamento da aplicação.
|
||||||
|
|
||||||
### 1.2 Proteção Contra Ataques de DNS Rebinding
|
### 3.2 Proteção Contra Ataques de DNS Rebinding
|
||||||
Para neutralizar ataques em que domínios externos tentam resolver para `127.0.0.1` e burlar a política de mesma origem do navegador:
|
Para neutralizar ataques em que domínios externos tentam resolver para `127.0.0.1` e burlar a política de mesma origem do navegador:
|
||||||
* O `CcspServer` valida expressamente o cabeçalho HTTP `Host`.
|
* O `CcspServer` valida expressamente o cabeçalho HTTP `Host` em **todas** as requisições (incluindo preflight `OPTIONS`).
|
||||||
* Somente requisições com `Host: 127.0.0.1:21325` ou `Host: localhost:21325` são aceitas; qualquer outro valor é rejeitado com status `403 Forbidden`.
|
* Somente requisições com `Host` correspondente a `127.0.0.1:{porta}`, `localhost:{porta}` ou `[::1]:{porta}` são aceitas; qualquer outro valor é rejeitado com status `403 Forbidden` (`INVALID_ORIGIN`).
|
||||||
|
|
||||||
### 1.3 Controle de Origem (CORS) e Preflight
|
|
||||||
* Requisições prévias HTTP `OPTIONS` são validadas contra as origens web homologadas.
|
|
||||||
* O cabeçalho `Access-Control-Allow-Origin` é devolvido estritamente com a origem solicitante autorizada (nunca com wildcard `*`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Autenticação e Autorização de Aplicações
|
## 4. Integridade Criptográfica do Registro de Aplicações (`ApplicationRegistry`)
|
||||||
|
|
||||||
Nenhuma aplicação web externa tem acesso a certificados digitais ou serviços de assinatura sem autorização prévia e explícita do usuário:
|
As aplicações autorizadas pelo usuário são armazenadas em disco sob um esquema de proteção em profundidade:
|
||||||
|
1. **Caminho:** `~/.cybercore/authorized-apps.json`.
|
||||||
1. **Handshake de Aprovação (`POST /ccsp/v1/authorize`):**
|
2. **Assinatura HMAC-SHA256:** Arquivo companheiro `authorized-apps.json.hmac` calcula o HMAC dos bytes JSON usando chave mestre de 256 bits gerada via `SecureRandom` e armazenada em `~/.cybercore/.vault_key`.
|
||||||
- Ao tentar comunicar-se pela primeira vez, a aplicação web deve fornecer seu `application_id` (UUID estável) e seu nome amigável.
|
3. **Permissões de Arquivo:** Aplicação de restrições POSIX (`0600` para arquivos e `0700` para diretórios) garantindo que apenas o usuário logado no sistema operacional possua direitos de leitura e escrita.
|
||||||
- O CyberCore Signer suspende a requisição e exibe um modal nativo no desktop informando o nome da aplicação e solicitando autorização do usuário.
|
4. **Detecção de Adulteração:** Se o arquivo JSON for modificado externamente (por malware ou editor de texto) sem a chave mestre, o carregamento descarta 100% das autorizações e emite alerta crítico de auditoria.
|
||||||
2. **Armazenamento Seguro de Permissões:**
|
|
||||||
- As aplicações autorizadas são gravadas no arquivo local `~/.cybercore/authorized-apps.json`.
|
|
||||||
- O usuário pode visualizar e **revogar a qualquer momento** a autorização de qualquer aplicação através da aba **Configurações** na interface gráfica.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Proteção Contra Ataques de Replay
|
## 5. Proteção de Memória e Ciclo de Vida do PIN/Chaves
|
||||||
|
|
||||||
Para impedir que requisições de assinatura capturadas por softwares maliciosos na máquina sejam reenviadas:
|
1. **Assinatura Exclusivamente Assistida:** Toda operação de assinatura exige interação humana voluntária. O agente nunca assina documentos em segundo plano de forma desassistida via API.
|
||||||
1. **Identificador Único (`nonce`):** Toda requisição ao `/ccsp/v1/sign` deve conter um UUID aleatório criptograficamente forte gerado pelo chamador.
|
2. **Inviolabilidade da Chave Privada:** As chaves privadas ICP-Brasil nunca são exportadas em formato de bytes brutos. Operações em certificados A3 utilizam handles nativos do hardware/middleware (PKCS#11 / MSCAPI).
|
||||||
2. **Janela de Validade Temporal (`expires_at`):** A requisição inclui uma estampa de tempo limite (TTL).
|
3. **Higienização de Memória Volátil:**
|
||||||
3. **Gerenciador de Nonces (`NonceManager`):**
|
|
||||||
- Mantém uma estrutura em memória com os identificadores recentes;
|
|
||||||
- Se o mesmo `nonce` for submetido mais de uma vez dentro da janela de validade, a operação é rejeitada com erro estruturado `REPLAY_DETECTED` (HTTP `409 Conflict`);
|
|
||||||
- Se a data/hora atual ultrapassar `expires_at`, a operação é rejeitada com erro `REQUEST_EXPIRED` (HTTP `422 Unprocessable Entity`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Confirmação Visual Obrigatória e Proteção de PIN/Chaves
|
|
||||||
|
|
||||||
1. **Assinatura Exclusivamente Assistida:** Toda operação de assinatura exige interação humana voluntária. O agente nunca assina documentos em segundo plano de forma silenciosa ou desassistida via API.
|
|
||||||
2. **Inviolabilidade da Chave Privada:** As chaves privadas ICP-Brasil nunca são exportadas em formato de bytes brutos. Operações em certificados A3 utilizam handles criptográficos do hardware, garantindo que a chave nunca deixe o chip criptográfico.
|
|
||||||
3. **Higienização Segura de Memória Volátil:**
|
|
||||||
- O PIN de smartcards e senhas de certificados A1 nunca são convertidos para strings imutáveis do Java (`java.lang.String`).
|
- O PIN de smartcards e senhas de certificados A1 nunca são convertidos para strings imutáveis do Java (`java.lang.String`).
|
||||||
- São manipulados estritamente como arrays de caracteres mutáveis `char[]`.
|
- São manipulados estritamente como arrays de caracteres mutáveis `char[]`.
|
||||||
- Imediatamente após o carregamento da chave, o array é sobrescrito com zeros através de `Arrays.fill(pinBuffer, (char) 0)`.
|
- Imediatamente após a autenticação do token, o array é sobrescrito com zeros via `Arrays.fill(pinBuffer, (char) 0)`.
|
||||||
4. **Remoção de Hardware (`TOKEN_REMOVED`):** Se o usuário desconectar o token USB ou retirar o cartão do leitor durante ou após a assinatura, o `SmartCardWatcher` força a limpeza imediata do cache de sessão e dos buffers do sistema operacional.
|
4. **Remoção de Hardware (`TOKEN_REMOVED`):** Se o usuário desconectar o token USB ou retirar o cartão do leitor, o `SmartCardWatcher` força a limpeza imediata do cache de sessão e dos buffers do sistema operacional.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Privacidade de Dados e Conformidade com a LGPD
|
## 6. Software Bill of Materials (SBOM) e Cadeia de Suprimentos
|
||||||
|
|
||||||
1. **Assinatura Descentralizada por Digest:** O CyberCore Signer opera prioritariamente recebendo apenas o hash SHA-256 do arquivo. O conteúdo completo do documento (contrato, laudo, prontuário) não transita pela API local nem é gravado em logs.
|
1. **CycloneDX SBOM:** O build padronizado do projeto gera automaticamente o SBOM nos formatos `target/bom.json` e `target/bom.xml` conforme a especificação CycloneDX 1.6.
|
||||||
2. **Mascaramento e Sigilo de CPF:** Nos selos visuais em PDF, o usuário pode configurar o modo de exibição de seu CPF (`CpfDisplayMode`):
|
2. **Análise Estática e Vulnerabilidades:** O pipeline de CI executa verificações contínuas de segurança:
|
||||||
- `FULL`: Exibição completa (`123.456.789-00`);
|
- **SpotBugs + FindSecBugs:** Análise estática profunda de vulnerabilidades criptográficas e injeções de código.
|
||||||
- `MASKED`: Dígitos sensíveis ocultados (`***.456.789-**`);
|
- **Enforcer Plugin:** Verificação rigorosa de versões mínimas do compilador e convergência de dependências transitivas.
|
||||||
- `NONE`: Remoção completa do CPF do gráfico visual do selo.
|
|
||||||
3. **Auditoria e Logs Transparentes:** Os logs do agente registram apenas identificadores de correlação (`request_id`), algoritmo utilizado e status da operação, sem armazenar hashes de documentos sigilosos ou dados pessoais desnecessários.
|
|
||||||
|
|||||||
@@ -30,24 +30,18 @@ Em conformidade com os requisitos do projeto, a interface **não é uma cópia d
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Grade Central de Ações (Hub 4x3)
|
## 3. Grade Central de Ações (Hub 3x2)
|
||||||
|
|
||||||
A interface principal organiza as operações em 12 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
|
A interface principal organiza as operações em 6 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
|
||||||
|
|
||||||
| Posição | Operação | Descrição |
|
| 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. |
|
| (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). |
|
| (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. |
|
| (2, 0) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
|
||||||
| (3, 0) | **Assinar em Lote** | Processamento simultâneo de múltiplos arquivos com barra de progresso em tempo real. |
|
| (0, 1) | **Gerenciar Selos** | Abertura do Designer e Gerenciador de Selos Digitais visuais para PAdES. |
|
||||||
| (0, 1) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
|
| (1, 1) | **Validar Assinatura** | Verificação de integridade criptográfica e conformidade do certificado digital. |
|
||||||
| (1, 1) | **Co-Assinar Documento** | Adição de nova assinatura a um documento previamente assinado por outro titular. |
|
| (2, 1) | **Sair do Agente** | Encerramento seguro da sessão do agente. |
|
||||||
| (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. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Guia de Padrão Visual e Design System SVG — CyberCore Signer
|
||||||
|
|
||||||
|
Este documento estabelece a especificação oficial de design, geometria, tokens de cores e regras de compatibilidade para todos os ícones e elementos gráficos vetoriais (**SVG**) do ecossistema **CyberCore Signer**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Princípios de Design
|
||||||
|
|
||||||
|
1. **Clareza e Precisão:** Cada ícone deve comunicar imediatamente sua função técnica (PDF, XML, assinatura embutida, validação, credenciais).
|
||||||
|
2. **Minimalismo e Geometria Pura:** Evitar ruídos visuais, nós desnecessários, filtros proprietários ou imagens bitmap embutidas (`<image>`).
|
||||||
|
3. **Consistência de Traço e Escala:** Espessuras de contorno previsíveis e alinhadas ao grid de 48x48 pixels.
|
||||||
|
4. **Interoperabilidade:** Totalmente compatível com editores vetoriais modernos (**Inkscape, Figma, Adobe Illustrator**) e com o parser nativo de JavaFX em `AssetManager`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Grid e Canvas Canônico
|
||||||
|
|
||||||
|
| Propriedade | Valor Padrão | Descrição |
|
||||||
|
|---|---|---|
|
||||||
|
| **ViewBox** | `0 0 48 48` | Padrão obrigatório para ícones da grade de ações e ferramentas laterais. |
|
||||||
|
| **Dimensões Base** | `width="48" height="48"` | Dimensões nominais 1:1. |
|
||||||
|
| **Área Segura** | `4px` de margem interna | O conteúdo vetorial deve se concentrar entre `(4, 4)` e `(44, 44)`. |
|
||||||
|
| **Raio de Canto Padrão** | `rx="3"` ou `rx="6"` | Arredondamentos harmônicos em cantos de janelas e etiquetas. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Paleta de Cores e Tokens Visuais
|
||||||
|
|
||||||
|
Os ícones utilizam uma paleta de cores corporativa baseada em Material Design e Tailwind Modern, com alto contraste sobre fundos claros e escuros:
|
||||||
|
|
||||||
|
| Token | Hexadecimal | Papel / Significado |
|
||||||
|
|---|---|---|
|
||||||
|
| **`color-brand-primary`** | `#0288d1` | Azul CyberCore. Traço padrão de folhas, botões principais e XML. |
|
||||||
|
| **`color-brand-deep`** | `#01579b` | Azul escuro para dobras e acentos de profundidade. |
|
||||||
|
| **`color-paper-fold`** | `#b3e5fc` | Preenchimento suave da dobra superior do documento padrão. |
|
||||||
|
| **`color-pdf-accent`** | `#d32f2f` | Vermelho institucional do formato Adobe PDF. |
|
||||||
|
| **`color-pdf-fold`** | `#ffcdd2` | Preenchimento suave da dobra do documento PDF. |
|
||||||
|
| **`color-success`** | `#10b981` / `#2e7d32` | Verde esmeralda para assinaturas válidas e verificações. |
|
||||||
|
| **`color-gold-seal`** | `#f59e0b` / `#d97706` | Dourado e âmbar para selos notariais e certificação digital. |
|
||||||
|
| **`color-paper-lines`** | `#b0bec5` / `#cfd8dc` | Linhas de texto representativas e divisórias neutras. |
|
||||||
|
| **`color-console-bg`** | `#1e293b` | Fundo de janelas de terminal (Logs). |
|
||||||
|
| **`color-danger-exit`** | `#d32f2f` | Seta e alertas de encerramento do agente. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Anatomia dos Ícones de Documentos
|
||||||
|
|
||||||
|
Para documentos com dobra (como PDF, XML, Atachada e Validador):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<!-- 1. Folha de documento com dobra -->
|
||||||
|
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z"
|
||||||
|
fill="#ffffff" stroke="[COR_BORDA]" stroke-width="2.5" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- 2. Dobra de canto superior direito -->
|
||||||
|
<path d="M 32 4 L 32 14 L 42 14 Z"
|
||||||
|
fill="[COR_FUNDO_DOBRA]" stroke="[COR_BORDA]" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- 3. Linhas de texto representativas -->
|
||||||
|
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- 4. Elemento distintivo inferior (Etiqueta, Caneta, Clipe ou Checkmark) -->
|
||||||
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="[COR_BADGE]"/>
|
||||||
|
<text x="25" y="39" font-family="'Segoe UI', Arial, sans-serif"
|
||||||
|
font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle">[TEXTO]</text>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Boas Práticas para Exportação no Inkscape / Figma
|
||||||
|
|
||||||
|
Ao criar ou editar ícones nas ferramentas vetoriais externas:
|
||||||
|
|
||||||
|
1. **Escolha a opção "SVG Simples" (Plain SVG) no Inkscape:**
|
||||||
|
- No Inkscape: `Arquivo → Salvar Como... → SVG Simples (*.svg)`.
|
||||||
|
- Isso elimina metadados de sessão (`sodipodi:namedview`, guias, zoom e histórico).
|
||||||
|
2. **Nunca incorpore imagens bitmap (`<image>` base64):**
|
||||||
|
- O aplicativo desktop deve conter exclusivamente geometria vetorial limpa (`path`, `rect`, `circle`, `line`, `polygon`, `polyline`, `text`).
|
||||||
|
3. **Suporte a `style="..."` inline:**
|
||||||
|
- O parser do CyberCore Signer (`AssetManager`) suporta tanto atributos XML puros (`fill="#..."`) quanto estilos inline em CSS (`style="fill:#...;stroke:#..."`).
|
||||||
|
4. **Mantenha o ViewBox Quadrado:**
|
||||||
|
- Preserve sempre `viewBox="0 0 48 48"` para garantir alinhamento perfeito nos botões e painéis da interface.
|
||||||
@@ -108,6 +108,23 @@
|
|||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
<includes>
|
||||||
|
<include>build-info.properties</include>
|
||||||
|
</includes>
|
||||||
|
</resource>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>false</filtering>
|
||||||
|
<excludes>
|
||||||
|
<exclude>build-info.properties</exclude>
|
||||||
|
</excludes>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
@@ -138,6 +155,7 @@
|
|||||||
<Implementation-Title>CyberCore Signer</Implementation-Title>
|
<Implementation-Title>CyberCore Signer</Implementation-Title>
|
||||||
<Implementation-Version>${project.version}</Implementation-Version>
|
<Implementation-Version>${project.version}</Implementation-Version>
|
||||||
<Implementation-Vendor>CyberCore</Implementation-Vendor>
|
<Implementation-Vendor>CyberCore</Implementation-Vendor>
|
||||||
|
<Build-Flavor>${cybercore.build.flavor}</Build-Flavor>
|
||||||
</manifestEntries>
|
</manifestEntries>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
@@ -157,6 +175,39 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>jacoco-check</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.70</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>BRANCH</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.45</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -19,15 +19,47 @@ public class CyberCoreSignerApplication {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CyberCoreSignerApplication.class);
|
private static final Logger logger = LoggerFactory.getLogger(CyberCoreSignerApplication.class);
|
||||||
|
|
||||||
|
private static final String BUILD_INFO_RESOURCE = "/build-info.properties";
|
||||||
|
private static String buildFlavor = "production";
|
||||||
private static boolean developmentMode = false;
|
private static boolean developmentMode = false;
|
||||||
private static boolean headlessMode = false;
|
private static boolean headlessMode = false;
|
||||||
private static boolean noWaitMode = false;
|
private static boolean noWaitMode = false;
|
||||||
|
private static int configuredPort = CcspServer.DEFAULT_PORT;
|
||||||
private static volatile CcspServer runningServer = null;
|
private static volatile CcspServer runningServer = null;
|
||||||
|
|
||||||
|
static {
|
||||||
|
loadBuildInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadBuildInfo() {
|
||||||
|
try (var is = CyberCoreSignerApplication.class.getResourceAsStream(BUILD_INFO_RESOURCE)) {
|
||||||
|
if (is != null) {
|
||||||
|
var props = new java.util.Properties();
|
||||||
|
props.load(is);
|
||||||
|
String flavor = props.getProperty("build.flavor");
|
||||||
|
if (flavor != null && !flavor.isBlank()) {
|
||||||
|
buildFlavor = flavor.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (java.io.IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBuildFlavor() {
|
||||||
|
return buildFlavor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setBuildFlavorForTesting(String flavor) {
|
||||||
|
buildFlavor = flavor;
|
||||||
|
}
|
||||||
|
|
||||||
public static CcspServer getRunningServer() {
|
public static CcspServer getRunningServer() {
|
||||||
return runningServer;
|
return runningServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getConfiguredPort() {
|
||||||
|
return configuredPort;
|
||||||
|
}
|
||||||
|
|
||||||
public static void stopServer() {
|
public static void stopServer() {
|
||||||
if (runningServer != null) {
|
if (runningServer != null) {
|
||||||
runningServer.stop();
|
runningServer.stop();
|
||||||
@@ -36,6 +68,17 @@ public class CyberCoreSignerApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// Previne conflitos nativos de UI Automation/Acessibilidade do Glass no Windows 11
|
||||||
|
if (System.getProperty("javafx.accessibility") == null) {
|
||||||
|
System.setProperty("javafx.accessibility", "false");
|
||||||
|
}
|
||||||
|
if (System.getProperty("glass.accessible.force") == null) {
|
||||||
|
System.setProperty("glass.accessible.force", "false");
|
||||||
|
}
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
logger.error("Exceção não tratada na thread '{}': {}", thread.getName(), throwable.getMessage(), throwable);
|
||||||
|
});
|
||||||
|
|
||||||
parseArguments(args);
|
parseArguments(args);
|
||||||
printBanner();
|
printBanner();
|
||||||
|
|
||||||
@@ -57,34 +100,18 @@ public class CyberCoreSignerApplication {
|
|||||||
logger.error("ALERTA CRÍTICO: Engine criptográfica JCA/JCE não está totalmente operacional!");
|
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
|
// 1. Inicializa o núcleo e o servidor CCSP imediatamente (alta velocidade)
|
||||||
boolean isHeadless = headlessMode || GraphicsEnvironment.isHeadless();
|
CertificateManager certManager = CertificateManager.createDefault();
|
||||||
|
if (developmentMode) {
|
||||||
if (isHeadless) {
|
logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema).");
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
SigningService signingService = new SigningService(certManager);
|
||||||
|
br.com.cybercore.signer.core.protocol.CcspUserPromptListener promptListener =
|
||||||
|
new br.com.cybercore.signer.ui.prompt.JavaFxPromptListener();
|
||||||
|
|
||||||
logger.info("CyberCore Signer finalizado.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void startHeadlessServer() {
|
|
||||||
try {
|
try {
|
||||||
CertificateManager certManager = CertificateManager.createDefault();
|
runningServer = new CcspServer(configuredPort, certManager, signingService,
|
||||||
if (developmentMode) {
|
br.com.cybercore.signer.core.security.ApplicationRegistry.getInstance(), promptListener);
|
||||||
logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema).");
|
|
||||||
}
|
|
||||||
SigningService signingService = new SigningService(certManager);
|
|
||||||
runningServer = new CcspServer(certManager, signingService, null);
|
|
||||||
runningServer.start();
|
runningServer.start();
|
||||||
logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort());
|
logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort());
|
||||||
|
|
||||||
@@ -92,22 +119,74 @@ public class CyberCoreSignerApplication {
|
|||||||
logger.info("Encerrando servidor local CCSP/1...");
|
logger.info("Encerrando servidor local CCSP/1...");
|
||||||
stopServer();
|
stopServer();
|
||||||
}));
|
}));
|
||||||
|
} catch (java.net.BindException e) {
|
||||||
// Mantém processo vivo para atender requisições web, a menos que em modo no-wait
|
logger.error("ALERTA DE COLISÃO DE PORTA: A porta {} já está em uso por outra instância do CyberCore Signer ou outro serviço. Para usar uma porta alternativa, utilize o parâmetro --port <porta> ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort);
|
||||||
if (!noWaitMode) {
|
|
||||||
Thread.currentThread().join();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e);
|
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
|
||||||
|
logger.error("ALERTA DE COLISÃO DE PORTA: A porta {} já está em uso por outra instância do CyberCore Signer ou outro serviço. Para usar uma porta alternativa, utilize o parâmetro --port <porta> ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort);
|
||||||
|
} else {
|
||||||
|
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 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) {
|
||||||
|
try {
|
||||||
|
logger.info("Iniciando interface gráfica desktop desacoplada...");
|
||||||
|
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
|
||||||
|
CyberCoreDesktopApplication.setSharedServices(runningServer, certManager, signingService);
|
||||||
|
Thread uiThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Application.launch(CyberCoreDesktopApplication.class, args);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Interface gráfica finalizada ou indisponível: {}", t.getMessage());
|
||||||
|
}
|
||||||
|
}, "cybercore-desktop-ui");
|
||||||
|
uiThread.setDaemon(false);
|
||||||
|
uiThread.start();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo serviço.", t.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("Executando em modo console / headless (sem interface gráfica).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mantém processo vivo para atender requisições web, a menos que em modo no-wait
|
||||||
|
if (!noWaitMode) {
|
||||||
|
try {
|
||||||
|
Thread.currentThread().join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("CyberCore Signer finalizado.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseArguments(String[] args) {
|
public static void parseArguments(String[] args) {
|
||||||
|
String envPort = System.getenv("CYBERCORE_PORT");
|
||||||
|
if (envPort != null && !envPort.isBlank()) {
|
||||||
|
try {
|
||||||
|
configuredPort = Integer.parseInt(envPort.trim());
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
String propPort = System.getProperty("cybercore.port");
|
||||||
|
if (propPort != null && !propPort.isBlank()) {
|
||||||
|
try {
|
||||||
|
configuredPort = Integer.parseInt(propPort.trim());
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (args != null) {
|
if (args != null) {
|
||||||
for (String arg : args) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
String arg = args[i];
|
||||||
if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) {
|
if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) {
|
||||||
|
if ("production".equalsIgnoreCase(buildFlavor)) {
|
||||||
|
logger.error("ALERTA DE SEGURANÇA: Modo de desenvolvimento (--dev) não é permitido em compilações de produção (flavor: production).");
|
||||||
|
throw new SecurityException("Modo de desenvolvimento (--dev) bloqueado em compilações de produção.");
|
||||||
|
}
|
||||||
developmentMode = true;
|
developmentMode = true;
|
||||||
} else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg)
|
} else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg)
|
||||||
|| "--cli".equalsIgnoreCase(arg)) {
|
|| "--cli".equalsIgnoreCase(arg)) {
|
||||||
@@ -115,6 +194,13 @@ public class CyberCoreSignerApplication {
|
|||||||
} else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg)
|
} else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg)
|
||||||
|| "--test".equalsIgnoreCase(arg)) {
|
|| "--test".equalsIgnoreCase(arg)) {
|
||||||
noWaitMode = true;
|
noWaitMode = true;
|
||||||
|
} else if (("--port".equalsIgnoreCase(arg) || "-p".equalsIgnoreCase(arg)) && i + 1 < args.length) {
|
||||||
|
try {
|
||||||
|
configuredPort = Integer.parseInt(args[++i].trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
String sanitized = args[i].replaceAll("[\r\n]", "");
|
||||||
|
logger.warn("Porta inválida especificada em --port: '{}'. Usando porta padrão {}.", sanitized, CcspServer.DEFAULT_PORT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
build.version=${project.version}
|
||||||
|
build.flavor=${cybercore.build.flavor}
|
||||||
@@ -34,4 +34,17 @@ class ArchitectureTest {
|
|||||||
|
|
||||||
rule.check(importedClasses);
|
rule.check(importedClasses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("O módulo Core não deve acessar o ciclo de vida do toolkit JavaFX (Application ou Platform)")
|
||||||
|
void coreShouldNotAccessJavaFxLifecycle() {
|
||||||
|
ArchRule rule = noClasses()
|
||||||
|
.that().resideInAPackage("br.com.cybercore.signer.core..")
|
||||||
|
.should().dependOnClassesThat()
|
||||||
|
.haveFullyQualifiedName("javafx.application.Application")
|
||||||
|
.orShould().dependOnClassesThat()
|
||||||
|
.haveFullyQualifiedName("javafx.application.Platform");
|
||||||
|
|
||||||
|
rule.check(importedClasses);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,43 @@ import static org.assertj.core.api.Assertions.assertThatCode;
|
|||||||
class CyberCoreSignerApplicationTest {
|
class CyberCoreSignerApplicationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Deve inicializar a aplicação com argumentos sem erros")
|
@DisplayName("Deve inicializar a aplicação com argumentos sem erros em compilação normal")
|
||||||
void shouldBootstrapWithoutErrors() {
|
void shouldBootstrapWithoutErrors() {
|
||||||
try {
|
try {
|
||||||
assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait"}))
|
assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--headless", "--no-wait", "--port", "29999"}))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
} finally {
|
} finally {
|
||||||
CyberCoreSignerApplication.stopServer();
|
CyberCoreSignerApplication.stopServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve bloquear o modo dev em compilações de produção")
|
||||||
|
void shouldBlockDevModeInProductionFlavor() {
|
||||||
|
CyberCoreSignerApplication.setBuildFlavorForTesting("production");
|
||||||
|
org.assertj.core.api.Assertions.assertThatThrownBy(() ->
|
||||||
|
CyberCoreSignerApplication.parseArguments(new String[]{"--dev"})
|
||||||
|
).isInstanceOf(SecurityException.class)
|
||||||
|
.hasMessageContaining("bloqueado em compilações de produção");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve permitir o modo dev quando o flavor for development")
|
||||||
|
void shouldAllowDevModeInDevelopmentFlavor() {
|
||||||
|
CyberCoreSignerApplication.setBuildFlavorForTesting("development");
|
||||||
|
org.assertj.core.api.Assertions.assertThatCode(() ->
|
||||||
|
CyberCoreSignerApplication.parseArguments(new String[]{"--dev"})
|
||||||
|
).doesNotThrowAnyException();
|
||||||
|
org.assertj.core.api.Assertions.assertThat(CyberCoreSignerApplication.isDevelopmentMode()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve realizar o parse correto do argumento --port")
|
||||||
|
void shouldParsePortArgument() {
|
||||||
|
CyberCoreSignerApplication.parseArguments(new String[]{"--port", "25555"});
|
||||||
|
org.assertj.core.api.Assertions.assertThat(CyberCoreSignerApplication.getConfiguredPort()).isEqualTo(25555);
|
||||||
|
|
||||||
|
CyberCoreSignerApplication.parseArguments(new String[]{"-p", "26666"});
|
||||||
|
org.assertj.core.api.Assertions.assertThat(CyberCoreSignerApplication.getConfiguredPort()).isEqualTo(26666);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,4 +61,41 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>jacoco-check</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.60</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>BRANCH</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.40</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ public class SigningService {
|
|||||||
if (hint != null && !hint.isBlank()) {
|
if (hint != null && !hint.isBlank()) {
|
||||||
Optional<ManagedCertificate> found = certificateManager.findByHint(hint);
|
Optional<ManagedCertificate> found = certificateManager.findByHint(hint);
|
||||||
if (found.isEmpty()) {
|
if (found.isEmpty()) {
|
||||||
// Tenta rediscovery caso um token tenha acabado de ser conectado
|
// Tenta rediscovery forçado caso um token tenha acabado de ser conectado
|
||||||
certificateManager.discoverCertificates();
|
certificateManager.discoverCertificates(true);
|
||||||
found = certificateManager.findByHint(hint);
|
found = certificateManager.findByHint(hint);
|
||||||
}
|
}
|
||||||
if (found.isPresent()) {
|
if (found.isPresent()) {
|
||||||
@@ -113,8 +113,8 @@ public class SigningService {
|
|||||||
// Sem hint: seleciona automaticamente se houver apenas um certificado operacional disponível
|
// Sem hint: seleciona automaticamente se houver apenas um certificado operacional disponível
|
||||||
List<ManagedCertificate> available = certificateManager.getAvailableCertificates();
|
List<ManagedCertificate> available = certificateManager.getAvailableCertificates();
|
||||||
if (available.isEmpty()) {
|
if (available.isEmpty()) {
|
||||||
// Tenta redescobrir
|
// Tenta redescobrir forçando consulta aos provedores
|
||||||
certificateManager.discoverCertificates();
|
certificateManager.discoverCertificates(true);
|
||||||
available = certificateManager.getAvailableCertificates();
|
available = certificateManager.getAvailableCertificates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package br.com.cybercore.signer.core.crypto.revocation;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache concorrente em memória com TTL (Time-To-Live) para respostas OCSP e listas CRL,
|
||||||
|
* evitando requisições de rede redundantes e respeitando tempos de atualização das ACs.
|
||||||
|
*/
|
||||||
|
public class RevocationCache<K, V> {
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private record CacheEntry<V>(V value, Instant expiresAt) {
|
||||||
|
boolean isExpired() {
|
||||||
|
return Instant.now().isAfter(expiresAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Armazena um item no cache com o TTL especificado.
|
||||||
|
*/
|
||||||
|
public void put(K key, V value, Duration ttl) {
|
||||||
|
Objects.requireNonNull(key, "key cannot be null");
|
||||||
|
Objects.requireNonNull(value, "value cannot be null");
|
||||||
|
Objects.requireNonNull(ttl, "ttl cannot be null");
|
||||||
|
|
||||||
|
Instant expiresAt = Instant.now().plus(ttl);
|
||||||
|
cache.put(key, new CacheEntry<>(value, expiresAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera um item do cache se ainda for válido; caso tenha expirado, remove e retorna vazio.
|
||||||
|
*/
|
||||||
|
public Optional<V> get(K key) {
|
||||||
|
Objects.requireNonNull(key, "key cannot be null");
|
||||||
|
CacheEntry<V> entry = cache.get(key);
|
||||||
|
if (entry == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (entry.isExpired()) {
|
||||||
|
cache.remove(key, entry);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(entry.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera o item em cache ou obtém um novo através do fetcher caso não exista ou tenha expirado.
|
||||||
|
*/
|
||||||
|
public <E extends Exception> V getOrFetch(K key, Duration ttl, ThrowingSupplier<V, E> fetcher) throws E {
|
||||||
|
Optional<V> existing = get(key);
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
return existing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
V fetched = fetcher.get();
|
||||||
|
if (fetched != null) {
|
||||||
|
put(key, fetched, ttl);
|
||||||
|
}
|
||||||
|
return fetched;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa todas as entradas expiradas do cache.
|
||||||
|
*/
|
||||||
|
public void evictExpired() {
|
||||||
|
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa integralmente o cache.
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna o número de entradas ativas ou pendentes no cache.
|
||||||
|
*/
|
||||||
|
public int size() {
|
||||||
|
return cache.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ThrowingSupplier<T, E extends Exception> {
|
||||||
|
T get() throws E;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package br.com.cybercore.signer.core.crypto.tsa;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
|
||||||
|
import org.bouncycastle.tsp.TSPAlgorithms;
|
||||||
|
import org.bouncycastle.tsp.TimeStampRequest;
|
||||||
|
import org.bouncycastle.tsp.TimeStampRequestGenerator;
|
||||||
|
import org.bouncycastle.tsp.TimeStampResponse;
|
||||||
|
import org.bouncycastle.tsp.TimeStampToken;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cliente resiliente de Carimbo do Tempo (TSA RFC 3161) com suporte a retentativas com backoff,
|
||||||
|
* timeout configurável e fallback automático para servidores secundários.
|
||||||
|
*/
|
||||||
|
public final class TsaClient {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TsaClient.class);
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
private final List<String> serverUrls;
|
||||||
|
private final Duration timeout;
|
||||||
|
private final int maxRetries;
|
||||||
|
private final Duration initialBackoff;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public TsaClient(List<String> serverUrls) {
|
||||||
|
this(serverUrls, Duration.ofSeconds(5), 2, Duration.ofMillis(100), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TsaClient(List<String> serverUrls, Duration timeout, int maxRetries, Duration initialBackoff, HttpClient customHttpClient) {
|
||||||
|
Objects.requireNonNull(serverUrls, "serverUrls cannot be null");
|
||||||
|
if (serverUrls.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("serverUrls cannot be empty");
|
||||||
|
}
|
||||||
|
this.serverUrls = Collections.unmodifiableList(new ArrayList<>(serverUrls));
|
||||||
|
this.timeout = timeout != null ? timeout : Duration.ofSeconds(5);
|
||||||
|
this.maxRetries = Math.max(0, maxRetries);
|
||||||
|
this.initialBackoff = initialBackoff != null ? initialBackoff : Duration.ofMillis(100);
|
||||||
|
this.httpClient = customHttpClient != null ? customHttpClient : HttpClient.newBuilder()
|
||||||
|
.connectTimeout(this.timeout)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solicita um carimbo de tempo RFC 3161 para o digest informado (SHA-256).
|
||||||
|
*/
|
||||||
|
public TimeStampToken requestTimestamp(byte[] digest) throws CertificateProviderException {
|
||||||
|
Objects.requireNonNull(digest, "digest cannot be null");
|
||||||
|
byte[] requestBytes = createTimestampRequest(digest);
|
||||||
|
|
||||||
|
Exception lastException = null;
|
||||||
|
|
||||||
|
for (String serverUrl : serverUrls) {
|
||||||
|
String sanitizedUrl = serverUrl.replaceAll("[\r\n]", "");
|
||||||
|
for (int attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
logger.debug("Enviando requisição RFC 3161 TSA para {} (tentativa {}/{})",
|
||||||
|
sanitizedUrl, attempt + 1, maxRetries + 1);
|
||||||
|
|
||||||
|
byte[] responseBytes = executeHttpRequest(serverUrl, requestBytes);
|
||||||
|
TimeStampResponse tsResponse = new TimeStampResponse(responseBytes);
|
||||||
|
tsResponse.validate(new TimeStampRequest(requestBytes));
|
||||||
|
|
||||||
|
TimeStampToken token = tsResponse.getTimeStampToken();
|
||||||
|
if (token == null) {
|
||||||
|
throw new IOException("TSA retornou resposta sem token: " + tsResponse.getStatusString());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Carimbo de tempo RFC 3161 obtido com sucesso da TSA {}", sanitizedUrl);
|
||||||
|
return token;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastException = e;
|
||||||
|
logger.warn("Falha ao comunicar com TSA {} (tentativa {}/{}): {}",
|
||||||
|
sanitizedUrl, attempt + 1, maxRetries + 1, e.getMessage());
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
try {
|
||||||
|
long backoffMs = initialBackoff.toMillis() * (1L << attempt);
|
||||||
|
Thread.sleep(backoffMs);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.COMMUNICATION_ERROR,
|
||||||
|
"Operação de carimbo de tempo interrompida",
|
||||||
|
ie
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.warn("Esgotadas tentativas para TSA {}. Tentando próximo servidor da lista...", sanitizedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.COMMUNICATION_ERROR,
|
||||||
|
"Falha ao obter carimbo de tempo RFC 3161 de todos os servidores TSA configurados: "
|
||||||
|
+ (lastException != null ? lastException.getMessage() : "sem resposta"),
|
||||||
|
lastException
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] createTimestampRequest(byte[] digest) {
|
||||||
|
try {
|
||||||
|
TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();
|
||||||
|
reqGen.setCertReq(true);
|
||||||
|
BigInteger nonce = BigInteger.valueOf(SECURE_RANDOM.nextLong()).abs();
|
||||||
|
ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier(TSPAlgorithms.SHA256.getId());
|
||||||
|
TimeStampRequest req = reqGen.generate(oid, digest, nonce);
|
||||||
|
return req.getEncoded();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Falha ao codificar requisição RFC 3161", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] executeHttpRequest(String serverUrl, byte[] requestBytes) throws IOException, InterruptedException {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(serverUrl))
|
||||||
|
.timeout(timeout)
|
||||||
|
.header("Content-Type", "application/timestamp-query")
|
||||||
|
.header("Accept", "application/timestamp-reply")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofByteArray(requestBytes))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new IOException("HTTP status " + response.statusCode() + " retornado pela TSA");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getServerUrls() {
|
||||||
|
return serverUrls;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
|||||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||||
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.io.RandomAccessRead;
|
||||||
|
import org.apache.pdfbox.io.RandomAccessReadBuffer;
|
||||||
|
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
@@ -83,7 +86,19 @@ public class PdfSigningService {
|
|||||||
*/
|
*/
|
||||||
public static int getPageCount(byte[] pdfBytes) throws IOException {
|
public static int getPageCount(byte[] pdfBytes) throws IOException {
|
||||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
try (RandomAccessReadBuffer rar = new RandomAccessReadBuffer(pdfBytes);
|
||||||
|
PDDocument doc = Loader.loadPDF(rar)) {
|
||||||
|
return doc.getNumberOfPages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna a quantidade total de páginas contidas no arquivo PDF.
|
||||||
|
*/
|
||||||
|
public static int getPageCount(File pdfFile) throws IOException {
|
||||||
|
Objects.requireNonNull(pdfFile, "Arquivo PDF não pode ser nulo.");
|
||||||
|
try (RandomAccessReadBufferedFile rar = new RandomAccessReadBufferedFile(pdfFile);
|
||||||
|
PDDocument doc = Loader.loadPDF(rar)) {
|
||||||
return doc.getNumberOfPages();
|
return doc.getNumberOfPages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +108,24 @@ public class PdfSigningService {
|
|||||||
*/
|
*/
|
||||||
public static PageDimensions getPageDimensions(byte[] pdfBytes, int pageIndex0Based) throws IOException {
|
public static PageDimensions getPageDimensions(byte[] pdfBytes, int pageIndex0Based) throws IOException {
|
||||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
try (RandomAccessReadBuffer rar = new RandomAccessReadBuffer(pdfBytes);
|
||||||
|
PDDocument doc = Loader.loadPDF(rar)) {
|
||||||
|
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
|
||||||
|
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
|
||||||
|
}
|
||||||
|
PDPage page = doc.getPage(pageIndex0Based);
|
||||||
|
PDRectangle mediaBox = page.getMediaBox();
|
||||||
|
return new PageDimensions(mediaBox.getWidth(), mediaBox.getHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna as dimensões em pontos (pt) da página informada do arquivo PDF.
|
||||||
|
*/
|
||||||
|
public static PageDimensions getPageDimensions(File pdfFile, int pageIndex0Based) throws IOException {
|
||||||
|
Objects.requireNonNull(pdfFile, "Arquivo PDF não pode ser nulo.");
|
||||||
|
try (RandomAccessReadBufferedFile rar = new RandomAccessReadBufferedFile(pdfFile);
|
||||||
|
PDDocument doc = Loader.loadPDF(rar)) {
|
||||||
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
|
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
|
||||||
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
|
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
|
||||||
}
|
}
|
||||||
@@ -108,7 +140,8 @@ public class PdfSigningService {
|
|||||||
*/
|
*/
|
||||||
public static BufferedImage renderPageImage(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException {
|
public static BufferedImage renderPageImage(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException {
|
||||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
try (RandomAccessReadBuffer rar = new RandomAccessReadBuffer(pdfBytes);
|
||||||
|
PDDocument doc = Loader.loadPDF(rar)) {
|
||||||
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
|
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
|
||||||
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
|
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
|
||||||
}
|
}
|
||||||
@@ -128,23 +161,55 @@ public class PdfSigningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assina digitalmente um arquivo PDF do sistema de arquivos e grava o resultado no arquivo de destino.
|
* Assina digitalmente um arquivo PDF do sistema de arquivos e grava o resultado no arquivo de destino,
|
||||||
|
* utilizando streaming direto via RandomAccessReadBufferedFile para minimizar o consumo de memória heap.
|
||||||
*/
|
*/
|
||||||
public void signPdfFile(File sourcePdf, File targetPdf, PdfSignatureOptions options) throws Exception {
|
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(sourcePdf, "Arquivo PDF de origem não pode ser nulo.");
|
||||||
Objects.requireNonNull(targetPdf, "Arquivo PDF de destino 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.");
|
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
|
||||||
|
|
||||||
byte[] inputBytes = Files.readAllBytes(sourcePdf.toPath());
|
if (!sourcePdf.exists() || !sourcePdf.isFile()) {
|
||||||
byte[] signedBytes = signPdf(inputBytes, options);
|
throw new IllegalArgumentException("Arquivo PDF de origem não existe: " + sourcePdf.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
if (targetPdf.getParentFile() != null) {
|
boolean sameFile = sourcePdf.getCanonicalPath().equals(targetPdf.getCanonicalPath());
|
||||||
targetPdf.getParentFile().mkdirs();
|
File parentDir = targetPdf.getParentFile();
|
||||||
|
if (parentDir != null && !parentDir.exists()) {
|
||||||
|
parentDir.mkdirs();
|
||||||
}
|
}
|
||||||
try (FileOutputStream fos = new FileOutputStream(targetPdf)) {
|
|
||||||
fos.write(signedBytes);
|
File effectiveTarget = sameFile
|
||||||
|
? Files.createTempFile("cybercore_signed_", ".pdf").toFile()
|
||||||
|
: targetPdf;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Calcula o hash SHA-256 via streaming em blocos de 64KB sem carregar o arquivo na RAM
|
||||||
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
|
try (InputStream is = Files.newInputStream(sourcePdf.toPath())) {
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
int read;
|
||||||
|
while ((read = is.read(buffer)) != -1) {
|
||||||
|
sha256.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] pdfDigest = sha256.digest();
|
||||||
|
|
||||||
|
try (RandomAccessReadBufferedFile rar = new RandomAccessReadBufferedFile(sourcePdf);
|
||||||
|
FileOutputStream fos = new FileOutputStream(effectiveTarget)) {
|
||||||
|
signPdfInternal(rar, fos, options, pdfDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sameFile) {
|
||||||
|
Files.move(effectiveTarget.toPath(), targetPdf.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (sameFile && effectiveTarget.exists()) {
|
||||||
|
Files.deleteIfExists(effectiveTarget.toPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info("PDF assinado com sucesso e salvo em: {}", targetPdf.getAbsolutePath());
|
|
||||||
|
logger.info("PDF assinado com sucesso e salvo em: {}", targetPdf.getAbsolutePath().replaceAll("[\r\n]", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,6 +220,39 @@ public class PdfSigningService {
|
|||||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||||
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
|
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
|
||||||
|
|
||||||
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] pdfDigest = sha256.digest(pdfBytes);
|
||||||
|
|
||||||
|
// Se o PDF for muito grande (> 2MB), usamos um arquivo temporário com RandomAccessReadBufferedFile
|
||||||
|
// para aliviar significativamente a pressão no Garbage Collector e economizar memória heap.
|
||||||
|
if (pdfBytes.length > 2 * 1024 * 1024) {
|
||||||
|
File tempSource = Files.createTempFile("cybercore_in_", ".pdf").toFile();
|
||||||
|
File tempTarget = Files.createTempFile("cybercore_out_", ".pdf").toFile();
|
||||||
|
try {
|
||||||
|
Files.write(tempSource.toPath(), pdfBytes);
|
||||||
|
try (RandomAccessReadBufferedFile rar = new RandomAccessReadBufferedFile(tempSource);
|
||||||
|
FileOutputStream fos = new FileOutputStream(tempTarget)) {
|
||||||
|
signPdfInternal(rar, fos, options, pdfDigest);
|
||||||
|
}
|
||||||
|
return Files.readAllBytes(tempTarget.toPath());
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(tempSource.toPath());
|
||||||
|
Files.deleteIfExists(tempTarget.toPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para PDFs de tamanho padrão (< 2MB), processa em memória com buffer pré-alocado
|
||||||
|
ByteArrayOutputStream finalOut = new ByteArrayOutputStream(pdfBytes.length + 65536);
|
||||||
|
try (RandomAccessReadBuffer rar = new RandomAccessReadBuffer(pdfBytes)) {
|
||||||
|
signPdfInternal(rar, finalOut, options, pdfDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
||||||
|
return finalOut.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void signPdfInternal(RandomAccessRead rar, OutputStream targetOut,
|
||||||
|
PdfSignatureOptions options, byte[] pdfDigest) throws Exception {
|
||||||
// 1. Localiza o certificado e o material criptográfico (chave e cadeia)
|
// 1. Localiza o certificado e o material criptográfico (chave e cadeia)
|
||||||
Optional<ManagedCertificate> certOpt = certificateManager.findById(options.certificateId());
|
Optional<ManagedCertificate> certOpt = certificateManager.findById(options.certificateId());
|
||||||
if (certOpt.isEmpty()) {
|
if (certOpt.isEmpty()) {
|
||||||
@@ -186,8 +284,6 @@ public class PdfSigningService {
|
|||||||
String rawName = cert.commonName() != null ? cert.commonName() : cert.alias();
|
String rawName = cert.commonName() != null ? cert.commonName() : cert.alias();
|
||||||
String signerName = X509CertificateUtils.extractHolderName(rawName);
|
String signerName = X509CertificateUtils.extractHolderName(rawName);
|
||||||
|
|
||||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] pdfDigest = sha256.digest(pdfBytes);
|
|
||||||
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
|
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
|
||||||
|
|
||||||
PdfSealTheme theme = options.sealTheme();
|
PdfSealTheme theme = options.sealTheme();
|
||||||
@@ -213,8 +309,7 @@ public class PdfSigningService {
|
|||||||
// preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
|
// preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
|
||||||
// o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
|
// o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
|
||||||
// das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
|
// das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
|
||||||
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
try (PDDocument doc = Loader.loadPDF(rar)) {
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
|
||||||
PDSignature signature = new PDSignature();
|
PDSignature signature = new PDSignature();
|
||||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||||
@@ -235,16 +330,13 @@ public class PdfSigningService {
|
|||||||
signatureOptions.setVisualSignature(visualTemplate);
|
signatureOptions.setVisualSignature(visualTemplate);
|
||||||
signatureOptions.setPage(targetPage);
|
signatureOptions.setPage(targetPage);
|
||||||
doc.addSignature(signature, signer, signatureOptions);
|
doc.addSignature(signature, signer, signatureOptions);
|
||||||
doc.saveIncremental(finalOut);
|
doc.saveIncremental(targetOut);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
doc.addSignature(signature, signer);
|
doc.addSignature(signature, signer);
|
||||||
doc.saveIncremental(finalOut);
|
doc.saveIncremental(targetOut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
|
||||||
return finalOut.toByteArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
|
private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
|
||||||
|
|||||||
@@ -116,7 +116,13 @@ public record CcspRequest(
|
|||||||
String targetElementTag,
|
String targetElementTag,
|
||||||
|
|
||||||
@JsonProperty("target_element_id")
|
@JsonProperty("target_element_id")
|
||||||
String targetElementId
|
String targetElementId,
|
||||||
|
|
||||||
|
@JsonProperty("document_size")
|
||||||
|
Long documentSize,
|
||||||
|
|
||||||
|
@JsonProperty("origin")
|
||||||
|
String origin
|
||||||
) implements Serializable {
|
) implements Serializable {
|
||||||
|
|
||||||
public static final String PROTOCOL_VERSION = "1";
|
public static final String PROTOCOL_VERSION = "1";
|
||||||
@@ -130,7 +136,8 @@ public record CcspRequest(
|
|||||||
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
null, null, null, null, null, null, null, null, null, null,
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
null, null, null, null, null, null, null, null, null, null);
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVersionSupported() {
|
public boolean isVersionSupported() {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
@@ -120,11 +121,8 @@ public class CcspServer {
|
|||||||
InetSocketAddress address = new InetSocketAddress(BIND_HOST, port);
|
InetSocketAddress address = new InetSocketAddress(BIND_HOST, port);
|
||||||
httpServer = HttpServer.create(address, 50);
|
httpServer = HttpServer.create(address, 50);
|
||||||
|
|
||||||
executor = Executors.newFixedThreadPool(8, r -> {
|
ThreadFactory virtualThreadFactory = Thread.ofVirtual().name("ccsp-virtual-worker-", 0).factory();
|
||||||
Thread t = new Thread(r, "ccsp-http-worker");
|
executor = Executors.newThreadPerTaskExecutor(virtualThreadFactory);
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
httpServer.setExecutor(executor);
|
httpServer.setExecutor(executor);
|
||||||
|
|
||||||
// Registra endpoints do protocolo CCSP/1
|
// Registra endpoints do protocolo CCSP/1
|
||||||
@@ -218,16 +216,20 @@ public class CcspServer {
|
|||||||
appId = getQueryParam(uri, "application_id");
|
appId = getQueryParam(uri, "application_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica autorização se app_id e origin forem informados
|
// Exige obrigatoriamente a identificação da aplicação para listagem de certificados
|
||||||
if (appId != null && !appId.isBlank()) {
|
if (appId == null || appId.isBlank()) {
|
||||||
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
sendErrorResponse(exchange, 401, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||||
if (!authorized) {
|
"application_id (via query param ou cabeçalho X-CyberCore-AppId) é obrigatório.");
|
||||||
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
|
return;
|
||||||
if (!approved) {
|
}
|
||||||
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
|
||||||
"Aplicação não autorizada pelo usuário.");
|
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,14 +352,17 @@ public class CcspServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Validação anti-replay (Nonce)
|
// 3. Validação anti-replay (Nonce obrigatório)
|
||||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
if (!accepted) {
|
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
return;
|
||||||
"Requisição duplicada detectada (replay prevenido).");
|
}
|
||||||
return;
|
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||||
}
|
if (!accepted) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
|
"Requisição duplicada detectada (replay prevenido).");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Validação de autorização da aplicação
|
// 4. Validação de autorização da aplicação
|
||||||
@@ -520,13 +525,17 @@ public class CcspServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
// Validação anti-replay (Nonce obrigatório)
|
||||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||||
if (!accepted) {
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||||
"Requisição duplicada detectada (replay prevenido).");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||||
|
if (!accepted) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
|
"Requisição duplicada detectada (replay prevenido).");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String appId = request.applicationId();
|
String appId = request.applicationId();
|
||||||
@@ -735,13 +744,17 @@ public class CcspServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
// Validação anti-replay (Nonce obrigatório)
|
||||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||||
if (!accepted) {
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||||
"Requisição duplicada detectada (replay prevenido).");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||||
|
if (!accepted) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
|
"Requisição duplicada detectada (replay prevenido).");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String appId = request.applicationId();
|
String appId = request.applicationId();
|
||||||
@@ -914,6 +927,7 @@ public class CcspServer {
|
|||||||
resp.put("status", "success");
|
resp.put("status", "success");
|
||||||
resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id());
|
resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id());
|
||||||
resp.put("seals", sealList);
|
resp.put("seals", sealList);
|
||||||
|
resp.put("themes", sealList);
|
||||||
|
|
||||||
sendJsonResponse(exchange, 200, resp);
|
sendJsonResponse(exchange, 200, resp);
|
||||||
}
|
}
|
||||||
@@ -1031,26 +1045,92 @@ public class CcspServer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleCors(HttpExchange exchange) throws IOException {
|
private boolean validateHost(HttpExchange exchange) throws IOException {
|
||||||
String origin = getOrigin(exchange);
|
String hostHeader = exchange.getRequestHeaders().getFirst("Host");
|
||||||
if (origin == null || origin.isBlank()) {
|
if (hostHeader == null || hostHeader.isBlank()) {
|
||||||
origin = "*";
|
logger.warn("Requisição CCSP rejeitada: cabeçalho 'Host' ausente.");
|
||||||
|
sendErrorResponse(exchange, 403, "INVALID_HOST", "Cabeçalho Host é obrigatório.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
|
String rawHost = hostHeader.trim();
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
String host;
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
if (rawHost.startsWith("[")) {
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
int closingBracket = rawHost.indexOf(']');
|
||||||
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
if (closingBracket != -1) {
|
||||||
|
host = rawHost.substring(0, closingBracket + 1);
|
||||||
|
} else {
|
||||||
|
host = rawHost;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int colonIdx = rawHost.indexOf(':');
|
||||||
|
host = colonIdx != -1 ? rawHost.substring(0, colonIdx) : rawHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAllowedHost = "127.0.0.1".equalsIgnoreCase(host)
|
||||||
|
|| "localhost".equalsIgnoreCase(host)
|
||||||
|
|| "[::1]".equalsIgnoreCase(host);
|
||||||
|
|
||||||
|
if (!isAllowedHost) {
|
||||||
|
logger.warn("ALERTA DE SEGURANÇA: Requisição CCSP rejeitada por suspeita de DNS Rebinding. Host: '{}'", hostHeader);
|
||||||
|
sendErrorResponse(exchange, 403, CcspErrorCode.INVALID_ORIGIN.name(), "Host não autorizado para loopback local.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleCors(HttpExchange exchange) throws IOException {
|
||||||
|
if (!validateHost(exchange)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
|
String origin = getOrigin(exchange);
|
||||||
|
|
||||||
|
if (origin == null || origin.isBlank()) {
|
||||||
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
exchange.sendResponseHeaders(204, -1);
|
||||||
|
exchange.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determina se a origem ou rota é elegível para CORS
|
||||||
|
boolean isHandshakePath = path.endsWith("/status") || path.endsWith("/authorize");
|
||||||
|
boolean isAuthorizedOrigin = applicationRegistry.findByOrigin(origin).isPresent() || isLocalOrigin(origin);
|
||||||
|
|
||||||
|
if (isHandshakePath || isAuthorizedOrigin) {
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
||||||
|
}
|
||||||
|
|
||||||
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
exchange.sendResponseHeaders(204, -1);
|
if (isHandshakePath || isAuthorizedOrigin) {
|
||||||
|
exchange.sendResponseHeaders(204, -1);
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(403, -1);
|
||||||
|
}
|
||||||
exchange.close();
|
exchange.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isLocalOrigin(String origin) {
|
||||||
|
if (origin == null || origin.isBlank()) return false;
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(origin);
|
||||||
|
String host = uri.getHost();
|
||||||
|
return "localhost".equalsIgnoreCase(host) || "127.0.0.1".equals(host);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String getOrigin(HttpExchange exchange) {
|
private String getOrigin(HttpExchange exchange) {
|
||||||
String origin = exchange.getRequestHeaders().getFirst("Origin");
|
String origin = exchange.getRequestHeaders().getFirst("Origin");
|
||||||
if (origin == null || origin.isBlank()) {
|
if (origin == null || origin.isBlank()) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package br.com.cybercore.signer.core.provider;
|
|||||||
|
|
||||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
|
import br.com.cybercore.signer.core.security.SmartCardWatcher;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -19,26 +20,82 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
* Gerenciador central de certificados digitais do CyberCore Signer.
|
* Gerenciador central de certificados digitais do CyberCore Signer.
|
||||||
* Responsável pela descoberta em múltiplos provedores (Windows Certificate Store, A1, PKCS#11),
|
* 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.
|
* validação de integridade, mapeamento de provedores e seleção segura para operações de assinatura.
|
||||||
|
* Implementa cache sob demanda com TTL e invalidação imediata em eventos de hardware (SmartCardWatcher).
|
||||||
*/
|
*/
|
||||||
public class CertificateManager {
|
public class CertificateManager implements SmartCardWatcher.CardEventListener {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CertificateManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(CertificateManager.class);
|
||||||
|
private static final long DEFAULT_CACHE_TTL_MS = 3_000L;
|
||||||
|
|
||||||
|
private final long cacheTtlMs;
|
||||||
private final List<CertificateProvider> providers = new CopyOnWriteArrayList<>();
|
private final List<CertificateProvider> providers = new CopyOnWriteArrayList<>();
|
||||||
private final Map<String, CertificateProvider> certificateToProviderMap = new ConcurrentHashMap<>();
|
private final Map<String, CertificateProvider> certificateToProviderMap = new ConcurrentHashMap<>();
|
||||||
private final Map<String, ManagedCertificate> cachedCertificates = new ConcurrentHashMap<>();
|
private final Map<String, ManagedCertificate> cachedCertificates = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private volatile long lastDiscoveryTimestamp = 0L;
|
||||||
|
private volatile List<ManagedCertificate> cachedDiscoveredList = Collections.emptyList();
|
||||||
|
|
||||||
public CertificateManager() {
|
public CertificateManager() {
|
||||||
|
this(DEFAULT_CACHE_TTL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertificateManager(long cacheTtlMs) {
|
||||||
|
this.cacheTtlMs = Math.max(0, cacheTtlMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalida explicitamente o cache em memória de certificados descobertos.
|
||||||
|
*/
|
||||||
|
public void invalidateCache() {
|
||||||
|
lastDiscoveryTimestamp = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vincula um SmartCardWatcher para invalidar o cache de certificados automaticamente
|
||||||
|
* assim que um token/cartão físico for inserido ou removido.
|
||||||
|
*/
|
||||||
|
public void attachSmartCardWatcher(SmartCardWatcher watcher) {
|
||||||
|
if (watcher != null) {
|
||||||
|
watcher.addListener(this);
|
||||||
|
logger.info("SmartCardWatcher vinculado com sucesso ao CertificateManager para invalidação automática de cache.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCardInserted(String terminalName) {
|
||||||
|
logger.info("Notificação de hardware: Smart Card inserido no terminal '{}'. Invalidando cache de certificados.", terminalName);
|
||||||
|
invalidateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCardRemoved(String terminalName) {
|
||||||
|
logger.info("Notificação de hardware: Smart Card removido do terminal '{}'. Invalidando cache de certificados.", terminalName);
|
||||||
|
invalidateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
||||||
|
* Suporta chaves de diagnóstico para isolamento de problemas de middleware:
|
||||||
|
* -Dcybercore.diag.no-wincert=true (não inicializa Windows Certificate Store)
|
||||||
|
* -Dcybercore.diag.no-pkcs11=true (não inicializa PKCS#11 / DLLs nativas)
|
||||||
*/
|
*/
|
||||||
public static CertificateManager createDefault() {
|
public static CertificateManager createDefault() {
|
||||||
CertificateManager manager = new CertificateManager();
|
CertificateManager manager = new CertificateManager();
|
||||||
manager.registerProvider(new WindowsCertificateStoreProvider());
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-wincert")) {
|
||||||
|
logger.warn("Diagnóstico: Provedor Windows Certificate Store DESATIVADO via -Dcybercore.diag.no-wincert=true");
|
||||||
|
} else {
|
||||||
|
manager.registerProvider(new WindowsCertificateStoreProvider());
|
||||||
|
}
|
||||||
|
|
||||||
manager.registerProvider(new Pkcs12CertificateProvider());
|
manager.registerProvider(new Pkcs12CertificateProvider());
|
||||||
manager.registerProvider(new Pkcs11CertificateProvider());
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-pkcs11")) {
|
||||||
|
logger.warn("Diagnóstico: Provedor PKCS#11 DESATIVADO via -Dcybercore.diag.no-pkcs11=true");
|
||||||
|
} else {
|
||||||
|
manager.registerProvider(new Pkcs11CertificateProvider());
|
||||||
|
}
|
||||||
|
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +107,7 @@ public class CertificateManager {
|
|||||||
// Evita duplicidade por providerId
|
// Evita duplicidade por providerId
|
||||||
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(provider.getProviderId()));
|
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(provider.getProviderId()));
|
||||||
providers.add(provider);
|
providers.add(provider);
|
||||||
|
invalidateCache();
|
||||||
logger.info("Provedor registrado no CertificateManager: {} [{}] (disponível={})",
|
logger.info("Provedor registrado no CertificateManager: {} [{}] (disponível={})",
|
||||||
provider.getDisplayName(), provider.getProviderId(), provider.isAvailable());
|
provider.getDisplayName(), provider.getProviderId(), provider.isAvailable());
|
||||||
}
|
}
|
||||||
@@ -61,6 +119,7 @@ public class CertificateManager {
|
|||||||
if (providerId != null) {
|
if (providerId != null) {
|
||||||
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
|
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
|
||||||
certificateToProviderMap.values().removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
|
certificateToProviderMap.values().removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
|
||||||
|
invalidateCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +129,23 @@ public class CertificateManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Executa a descoberta de certificados em todos os provedores operacionais.
|
* Executa a descoberta de certificados em todos os provedores operacionais.
|
||||||
* Consolida os resultados, atualiza o mapeamento de provedores e ordena por relevância/disponibilidade.
|
* Utiliza cache sob demanda se a última descoberta ocorreu dentro da janela de TTL.
|
||||||
*/
|
*/
|
||||||
public synchronized List<ManagedCertificate> discoverCertificates() {
|
public synchronized List<ManagedCertificate> discoverCertificates() {
|
||||||
|
return discoverCertificates(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa a descoberta de certificados em todos os provedores operacionais, com opção
|
||||||
|
* de forçar o recarregamento ignorando o TTL do cache.
|
||||||
|
*/
|
||||||
|
public synchronized List<ManagedCertificate> discoverCertificates(boolean forceRefresh) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (!forceRefresh && (now - lastDiscoveryTimestamp < cacheTtlMs) && !cachedDiscoveredList.isEmpty()) {
|
||||||
|
logger.debug("Retornando lista de certificados a partir do cache sob demanda (TTL: {} ms).", cacheTtlMs);
|
||||||
|
return cachedDiscoveredList;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Iniciando descoberta de certificados digitais nos provedores ativos...");
|
logger.info("Iniciando descoberta de certificados digitais nos provedores ativos...");
|
||||||
|
|
||||||
certificateToProviderMap.clear();
|
certificateToProviderMap.clear();
|
||||||
@@ -104,8 +177,11 @@ public class CertificateManager {
|
|||||||
.comparing((ManagedCertificate c) -> c.status() == CertificateStatus.AVAILABLE ? 0 : 1)
|
.comparing((ManagedCertificate c) -> c.status() == CertificateStatus.AVAILABLE ? 0 : 1)
|
||||||
.thenComparing(ManagedCertificate::commonName, String.CASE_INSENSITIVE_ORDER));
|
.thenComparing(ManagedCertificate::commonName, String.CASE_INSENSITIVE_ORDER));
|
||||||
|
|
||||||
|
cachedDiscoveredList = Collections.unmodifiableList(discovered);
|
||||||
|
lastDiscoveryTimestamp = System.currentTimeMillis();
|
||||||
|
|
||||||
logger.info("Descoberta concluída: {} certificado(s) total no agente.", discovered.size());
|
logger.info("Descoberta concluída: {} certificado(s) total no agente.", discovered.size());
|
||||||
return Collections.unmodifiableList(discovered);
|
return cachedDiscoveredList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,6 +258,7 @@ public class CertificateManager {
|
|||||||
* Solicita recarregamento de todos os provedores cadastrados.
|
* Solicita recarregamento de todos os provedores cadastrados.
|
||||||
*/
|
*/
|
||||||
public void refreshAll() {
|
public void refreshAll() {
|
||||||
|
invalidateCache();
|
||||||
for (CertificateProvider provider : providers) {
|
for (CertificateProvider provider : providers) {
|
||||||
try {
|
try {
|
||||||
provider.refresh();
|
provider.refresh();
|
||||||
@@ -189,6 +266,6 @@ public class CertificateManager {
|
|||||||
logger.warn("Erro ao recarregar provedor '{}': {}", provider.getProviderId(), e.getMessage());
|
logger.warn("Erro ao recarregar provedor '{}': {}", provider.getProviderId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
discoverCertificates();
|
discoverCertificates(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,14 @@ public class Pkcs11CertificateProvider implements CertificateProvider {
|
|||||||
"C:/Windows/System32/aetpkss1.dll", // SafeSign AET
|
"C:/Windows/System32/aetpkss1.dll", // SafeSign AET
|
||||||
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
|
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
|
||||||
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
|
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
|
||||||
|
"C:/SoftHSM2/lib/softhsm2.dll", // SoftHSM2 Windows
|
||||||
// Linux
|
// Linux
|
||||||
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
|
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
|
||||||
"/usr/lib/opensc-pkcs11.so",
|
"/usr/lib/opensc-pkcs11.so",
|
||||||
"/usr/lib/libaetpkss.so"
|
"/usr/lib/libaetpkss.so",
|
||||||
|
"/usr/lib/softhsm/libsofthsm2.so", // SoftHSM2 Debian/Ubuntu
|
||||||
|
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so", // SoftHSM2 Multiarch
|
||||||
|
"/usr/local/lib/softhsm/libsofthsm2.so"
|
||||||
};
|
};
|
||||||
|
|
||||||
private Provider sunPkcs11Provider;
|
private Provider sunPkcs11Provider;
|
||||||
@@ -79,6 +83,50 @@ public class Pkcs11CertificateProvider implements CertificateProvider {
|
|||||||
this.keyStore = keyStore;
|
this.keyStore = keyStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa dinamicamente o provider SunPKCS11 em memória para uma biblioteca específica,
|
||||||
|
* slot e PIN, sem expor ou gravar credenciais em disco.
|
||||||
|
*/
|
||||||
|
public synchronized void initializeWithPin(String libraryPath, int slotIndex, char[] pin) throws CertificateProviderException {
|
||||||
|
Objects.requireNonNull(libraryPath, "Caminho da biblioteca PKCS#11 não pode ser nulo.");
|
||||||
|
File f = new File(libraryPath);
|
||||||
|
if (!f.exists() || !f.canRead()) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.PKCS11_ERROR,
|
||||||
|
"Biblioteca PKCS#11 não encontrada ou sem permissão de leitura."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Provider prototype = Security.getProvider("SunPKCS11");
|
||||||
|
if (prototype == null) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
|
||||||
|
"Provider SunPKCS11 (jdk.crypto.cryptoki) não disponível nesta JVM."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String config = "--name = CyberCore-PKCS11-" + System.currentTimeMillis() + "\n"
|
||||||
|
+ "library = " + libraryPath + "\n"
|
||||||
|
+ "slotListIndex = " + slotIndex + "\n";
|
||||||
|
Provider configured = prototype.configure(config);
|
||||||
|
KeyStore ks = KeyStore.getInstance("PKCS11", configured);
|
||||||
|
ks.load(null, pin);
|
||||||
|
|
||||||
|
this.sunPkcs11Provider = configured;
|
||||||
|
this.keyStore = ks;
|
||||||
|
this.detectedLibraryPath = libraryPath;
|
||||||
|
logger.info("Provedor SunPKCS11 inicializado com sucesso para o slot {}", slotIndex);
|
||||||
|
} catch (CertificateProviderException cpe) {
|
||||||
|
throw cpe;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Falha ao inicializar o provedor SunPKCS11: {}", e.getMessage(), e);
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.PKCS11_ERROR,
|
||||||
|
"Falha ao inicializar o dispositivo PKCS#11: " + e.getMessage(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getProviderId() {
|
public String getProviderId() {
|
||||||
return PROVIDER_ID;
|
return PROVIDER_ID;
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ public final class ApplicationRegistry {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ApplicationRegistry.class);
|
private static final Logger logger = LoggerFactory.getLogger(ApplicationRegistry.class);
|
||||||
private static final String DEFAULT_FILE_NAME = "authorized-apps.json";
|
private static final String DEFAULT_FILE_NAME = "authorized-apps.json";
|
||||||
|
private static final String HMAC_FILE_SUFFIX = ".hmac";
|
||||||
|
|
||||||
private static final ApplicationRegistry INSTANCE = new ApplicationRegistry();
|
private static final ApplicationRegistry INSTANCE = new ApplicationRegistry();
|
||||||
|
|
||||||
private final Path storagePath;
|
private final Path storagePath;
|
||||||
|
private final HmacIntegrityManager integrityManager;
|
||||||
private final Map<String, AuthorizedApplication> applications = new ConcurrentHashMap<>();
|
private final Map<String, AuthorizedApplication> applications = new ConcurrentHashMap<>();
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@@ -37,15 +39,28 @@ public final class ApplicationRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ApplicationRegistry() {
|
public ApplicationRegistry() {
|
||||||
this(resolveDefaultStoragePath());
|
this(resolveDefaultStoragePath(), new HmacIntegrityManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplicationRegistry(Path storagePath) {
|
public ApplicationRegistry(Path storagePath) {
|
||||||
|
this(storagePath, new HmacIntegrityManager(resolveKeyPathForStorage(storagePath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationRegistry(Path storagePath, HmacIntegrityManager integrityManager) {
|
||||||
this.storagePath = storagePath;
|
this.storagePath = storagePath;
|
||||||
|
this.integrityManager = integrityManager != null ? integrityManager : new HmacIntegrityManager();
|
||||||
this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Path resolveKeyPathForStorage(Path storagePath) {
|
||||||
|
if (storagePath == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Path parent = storagePath.getParent();
|
||||||
|
return parent != null ? parent.resolve(".vault_key") : null;
|
||||||
|
}
|
||||||
|
|
||||||
private static Path resolveDefaultStoragePath() {
|
private static Path resolveDefaultStoragePath() {
|
||||||
String userHome = System.getProperty("user.home", ".");
|
String userHome = System.getProperty("user.home", ".");
|
||||||
return Path.of(userHome, ".cybercore", DEFAULT_FILE_NAME);
|
return Path.of(userHome, ".cybercore", DEFAULT_FILE_NAME);
|
||||||
@@ -108,8 +123,15 @@ public final class ApplicationRegistry {
|
|||||||
return Collections.unmodifiableList(new ArrayList<>(applications.values()));
|
return Collections.unmodifiableList(new ArrayList<>(applications.values()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path resolveHmacPath() {
|
||||||
|
if (storagePath == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Path.of(storagePath.toString() + HMAC_FILE_SUFFIX);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carrega as aplicações persistidas em disco.
|
* Carrega as aplicações persistidas em disco verificando a integridade por HMAC-SHA256.
|
||||||
*/
|
*/
|
||||||
public synchronized void load() {
|
public synchronized void load() {
|
||||||
if (storagePath == null || !Files.exists(storagePath)) {
|
if (storagePath == null || !Files.exists(storagePath)) {
|
||||||
@@ -118,22 +140,42 @@ public final class ApplicationRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
byte[] fileBytes = Files.readAllBytes(storagePath);
|
||||||
|
Path hmacPath = resolveHmacPath();
|
||||||
|
|
||||||
|
if (hmacPath != null && Files.exists(hmacPath)) {
|
||||||
|
String expectedHmac = Files.readString(hmacPath).trim();
|
||||||
|
boolean valid = integrityManager.verifyHmac(fileBytes, expectedHmac);
|
||||||
|
if (!valid) {
|
||||||
|
logger.error("ALERTA DE AUDITORIA: Adulteração detectada no arquivo de aplicações autorizadas {}. O HMAC diverge do esperado. Todas as autorizações foram invalidadas.", storagePath);
|
||||||
|
applications.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (hmacPath != null) {
|
||||||
|
// Se existe o JSON mas não o HMAC correspondente (migração/inicialização segura)
|
||||||
|
logger.warn("ALERTA DE AUDITORIA: Arquivo {} sem assinatura HMAC de integridade. Calculando e gerando assinatura inicial.", storagePath);
|
||||||
|
String newHmac = integrityManager.computeHmacHex(fileBytes);
|
||||||
|
Files.writeString(hmacPath, newHmac);
|
||||||
|
HmacIntegrityManager.restrictPermissions(hmacPath);
|
||||||
|
}
|
||||||
|
|
||||||
List<AuthorizedApplication> list = objectMapper.readValue(
|
List<AuthorizedApplication> list = objectMapper.readValue(
|
||||||
storagePath.toFile(),
|
fileBytes,
|
||||||
new TypeReference<List<AuthorizedApplication>>() {}
|
new TypeReference<List<AuthorizedApplication>>() {}
|
||||||
);
|
);
|
||||||
applications.clear();
|
applications.clear();
|
||||||
for (AuthorizedApplication app : list) {
|
for (AuthorizedApplication app : list) {
|
||||||
applications.put(app.applicationId(), app);
|
applications.put(app.applicationId(), app);
|
||||||
}
|
}
|
||||||
logger.info("Carregadas {} aplicação(ões) autorizada(s) de {}", applications.size(), storagePath);
|
logger.info("Carregadas {} aplicação(ões) autorizada(s) íntegras de {}", applications.size(), storagePath);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Falha ao ler aplicações autorizadas de {}: {}", storagePath, e.getMessage());
|
logger.warn("Falha ao ler aplicações autorizadas de {}: {}", storagePath, e.getMessage());
|
||||||
|
applications.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Salva as aplicações atuais em disco.
|
* Salva as aplicações atuais em disco com assinatura HMAC e permissões de acesso restritas.
|
||||||
*/
|
*/
|
||||||
public synchronized void save() {
|
public synchronized void save() {
|
||||||
if (storagePath == null) {
|
if (storagePath == null) {
|
||||||
@@ -144,12 +186,23 @@ public final class ApplicationRegistry {
|
|||||||
Path parent = storagePath.getParent();
|
Path parent = storagePath.getParent();
|
||||||
if (parent != null && !Files.exists(parent)) {
|
if (parent != null && !Files.exists(parent)) {
|
||||||
Files.createDirectories(parent);
|
Files.createDirectories(parent);
|
||||||
|
HmacIntegrityManager.restrictPermissions(parent);
|
||||||
}
|
}
|
||||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(
|
|
||||||
storagePath.toFile(),
|
byte[] jsonBytes = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(
|
||||||
new ArrayList<>(applications.values())
|
new ArrayList<>(applications.values())
|
||||||
);
|
);
|
||||||
logger.debug("Aplicações autorizadas salvas com sucesso em {}", storagePath);
|
Files.write(storagePath, jsonBytes);
|
||||||
|
HmacIntegrityManager.restrictPermissions(storagePath);
|
||||||
|
|
||||||
|
Path hmacPath = resolveHmacPath();
|
||||||
|
if (hmacPath != null) {
|
||||||
|
String hmacHex = integrityManager.computeHmacHex(jsonBytes);
|
||||||
|
Files.writeString(hmacPath, hmacHex);
|
||||||
|
HmacIntegrityManager.restrictPermissions(hmacPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Aplicações autorizadas e HMAC salvos com sucesso em {}", storagePath);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Erro ao salvar aplicações autorizadas em {}: {}", storagePath, e.getMessage());
|
logger.error("Erro ao salvar aplicações autorizadas em {}: {}", storagePath, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package br.com.cybercore.signer.core.security;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gerenciador de integridade criptográfica por HMAC-SHA256 para arquivos de configuração e
|
||||||
|
* autorizações locais, com custódia de chave mestre isolada e permissões de arquivo restritas.
|
||||||
|
*/
|
||||||
|
public class HmacIntegrityManager {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(HmacIntegrityManager.class);
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
private static final int KEY_SIZE_BYTES = 32;
|
||||||
|
private static final String DEFAULT_KEY_FILE = ".vault_key";
|
||||||
|
|
||||||
|
private final Path keyPath;
|
||||||
|
private byte[] secretKey;
|
||||||
|
|
||||||
|
public HmacIntegrityManager() {
|
||||||
|
this(resolveDefaultKeyPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public HmacIntegrityManager(Path keyPath) {
|
||||||
|
this.keyPath = keyPath;
|
||||||
|
initSecretKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path resolveDefaultKeyPath() {
|
||||||
|
String userHome = System.getProperty("user.home", ".");
|
||||||
|
return Path.of(userHome, ".cybercore", DEFAULT_KEY_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void initSecretKey() {
|
||||||
|
if (keyPath == null) {
|
||||||
|
byte[] ephemeralKey = new byte[KEY_SIZE_BYTES];
|
||||||
|
SECURE_RANDOM.nextBytes(ephemeralKey);
|
||||||
|
this.secretKey = ephemeralKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Files.exists(keyPath)) {
|
||||||
|
byte[] loaded = Files.readAllBytes(keyPath);
|
||||||
|
if (loaded.length >= KEY_SIZE_BYTES) {
|
||||||
|
this.secretKey = loaded;
|
||||||
|
restrictPermissions(keyPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gera nova chave criptográfica forte
|
||||||
|
byte[] newKey = new byte[KEY_SIZE_BYTES];
|
||||||
|
SECURE_RANDOM.nextBytes(newKey);
|
||||||
|
this.secretKey = newKey;
|
||||||
|
|
||||||
|
Path parent = keyPath.getParent();
|
||||||
|
if (parent != null && !Files.exists(parent)) {
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
restrictPermissions(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.write(keyPath, newKey);
|
||||||
|
restrictPermissions(keyPath);
|
||||||
|
logger.info("Chave mestre de integridade inicializada com permissões restritas em {}", keyPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Falha ao persistir chave em {}. Usando chave em memória: {}", keyPath, e.getMessage());
|
||||||
|
byte[] fallbackKey = new byte[KEY_SIZE_BYTES];
|
||||||
|
SECURE_RANDOM.nextBytes(fallbackKey);
|
||||||
|
this.secretKey = fallbackKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula o HMAC-SHA256 dos dados informados e devolve em hexadecimal.
|
||||||
|
*/
|
||||||
|
public String computeHmacHex(byte[] data) {
|
||||||
|
if (data == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(secretKey, HMAC_ALGORITHM);
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] hmacBytes = mac.doFinal(data);
|
||||||
|
return HexFormat.of().formatHex(hmacBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Erro ao calcular HMAC-SHA256: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica a integridade dos dados contra um HMAC esperado usando comparação em tempo constante
|
||||||
|
* para mitigar timing attacks.
|
||||||
|
*/
|
||||||
|
public boolean verifyHmac(byte[] data, String expectedHex) {
|
||||||
|
if (data == null || expectedHex == null || expectedHex.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String calculatedHex = computeHmacHex(data);
|
||||||
|
byte[] expectedBytes = expectedHex.trim().getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] calculatedBytes = calculatedHex.getBytes(StandardCharsets.UTF_8);
|
||||||
|
return MessageDigest.isEqual(expectedBytes, calculatedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restringe as permissões do arquivo/diretório para acesso exclusivo do usuário atual (0600 / 0700).
|
||||||
|
*/
|
||||||
|
public static void restrictPermissions(Path path) {
|
||||||
|
if (path == null || !Files.exists(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = path.toFile();
|
||||||
|
// Proteção padrão Java SE (funciona em Windows e Unix)
|
||||||
|
boolean ok = file.setReadable(false, false);
|
||||||
|
ok = file.setReadable(true, true) && ok;
|
||||||
|
ok = file.setWritable(false, false) && ok;
|
||||||
|
ok = file.setWritable(true, true) && ok;
|
||||||
|
ok = file.setExecutable(false, false) && ok;
|
||||||
|
if (!ok) {
|
||||||
|
logger.debug("Não foi possível ajustar todos os atributos em {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissões POSIX estritas (Linux / macOS)
|
||||||
|
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
|
||||||
|
try {
|
||||||
|
boolean isDir = Files.isDirectory(path);
|
||||||
|
Set<PosixFilePermission> permissions = isDir
|
||||||
|
? EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE)
|
||||||
|
: EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
|
||||||
|
Files.setPosixFilePermissions(path, permissions);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.debug("Não foi possível aplicar atributos POSIX em {}: {}", path, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
*/
|
*/
|
||||||
public class NonceManager {
|
public class NonceManager {
|
||||||
|
|
||||||
|
public static final Duration DEFAULT_CLOCK_SKEW = Duration.ofSeconds(30);
|
||||||
|
|
||||||
private final Duration windowDuration;
|
private final Duration windowDuration;
|
||||||
private final ConcurrentHashMap<String, Instant> usedNonces = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Instant> usedNonces = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@@ -21,6 +23,34 @@ public class NonceManager {
|
|||||||
this(Duration.ofSeconds(120));
|
this(Duration.ofSeconds(120));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida um timestamp de requisição em relação ao relógio local, considerando a janela
|
||||||
|
* de validade e uma margem segura de clock skew (tolerância a desvio de relógio entre cliente e agente).
|
||||||
|
*
|
||||||
|
* @param requestTimestamp Instant da requisição informado pelo cliente.
|
||||||
|
* @param clockSkew Tolerância de tolerância a skew de relógio.
|
||||||
|
* @return {@code true} se o timestamp estiver na faixa válida permitida.
|
||||||
|
*/
|
||||||
|
public boolean isTimestampValid(Instant requestTimestamp, Duration clockSkew) {
|
||||||
|
if (requestTimestamp == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Duration skew = clockSkew != null ? clockSkew : DEFAULT_CLOCK_SKEW;
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant earliestAllowed = now.minus(windowDuration).minus(skew);
|
||||||
|
Instant latestAllowed = now.plus(skew);
|
||||||
|
|
||||||
|
return !requestTimestamp.isBefore(earliestAllowed) && !requestTimestamp.isAfter(latestAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTimestampValid(Instant requestTimestamp) {
|
||||||
|
return isTimestampValid(requestTimestamp, DEFAULT_CLOCK_SKEW);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getWindowDuration() {
|
||||||
|
return windowDuration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenta registrar um nonce.
|
* Tenta registrar um nonce.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package br.com.cybercore.signer.core.security;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -12,15 +14,19 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer.
|
* Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer.
|
||||||
* Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3
|
* Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3
|
||||||
* exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão".
|
* exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão".
|
||||||
|
* Os PINs são escopados por aplicação web (applicationId + certId) e possuem expiração por inatividade (TTL de 15 min).
|
||||||
* Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte.
|
* Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte.
|
||||||
*/
|
*/
|
||||||
public class PinSessionManager {
|
public class PinSessionManager {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class);
|
||||||
|
private static final long SESSION_TTL_MINUTES = 15;
|
||||||
|
|
||||||
private static final PinSessionManager INSTANCE = new PinSessionManager();
|
private static final PinSessionManager INSTANCE = new PinSessionManager();
|
||||||
|
|
||||||
private final Map<String, char[]> sessionPins = new ConcurrentHashMap<>();
|
private record PinEntry(char[] pin, Instant lastAccessedAt) {}
|
||||||
|
|
||||||
|
private final Map<String, PinEntry> sessionPins = new ConcurrentHashMap<>();
|
||||||
private volatile boolean alwaysPromptPin = false;
|
private volatile boolean alwaysPromptPin = false;
|
||||||
|
|
||||||
public PinSessionManager() {
|
public PinSessionManager() {
|
||||||
@@ -30,63 +36,107 @@ public class PinSessionManager {
|
|||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildKey(String applicationId, String certId) {
|
||||||
|
String appId = (applicationId != null && !applicationId.isBlank()) ? applicationId.trim() : "default";
|
||||||
|
return appId + "::" + certId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Salva o PIN de um certificado/token para a sessão atual em memória volátil.
|
* Salva o PIN associado a uma aplicação específica e certificado.
|
||||||
*/
|
*/
|
||||||
public synchronized void savePin(String certId, char[] pin) {
|
public synchronized void savePin(String applicationId, String certId, char[] pin) {
|
||||||
Objects.requireNonNull(certId, "certId must not be null");
|
Objects.requireNonNull(certId, "certId must not be null");
|
||||||
|
String key = buildKey(applicationId, certId);
|
||||||
if (pin == null || pin.length == 0) {
|
if (pin == null || pin.length == 0) {
|
||||||
clearPin(certId);
|
clearPin(applicationId, certId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limpa PIN anterior caso exista
|
clearPin(applicationId, certId);
|
||||||
clearPin(certId);
|
sessionPins.put(key, new PinEntry(pin.clone(), Instant.now()));
|
||||||
|
logger.debug("PIN salvo em sessão para app '{}' e cert '{}'. Tamanho: {} dígitos", applicationId, certId, pin.length);
|
||||||
sessionPins.put(certId, pin.clone());
|
|
||||||
logger.debug("PIN salvo em memória de sessão para o certificado '{}'. Tamanho: {} dígitos", certId, pin.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recupera o PIN salvo em sessão para o certificado informado.
|
* Salva o PIN no escopo padrão (compatibilidade).
|
||||||
*/
|
*/
|
||||||
public synchronized char[] getPin(String certId) {
|
public synchronized void savePin(String certId, char[] pin) {
|
||||||
|
savePin("default", certId, pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera o PIN salvo em sessão para a aplicação e certificado especificados.
|
||||||
|
*/
|
||||||
|
public synchronized char[] getPin(String applicationId, String certId) {
|
||||||
if (certId == null || alwaysPromptPin) {
|
if (certId == null || alwaysPromptPin) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
char[] stored = sessionPins.get(certId);
|
String key = buildKey(applicationId, certId);
|
||||||
return stored != null ? stored.clone() : null;
|
PinEntry entry = sessionPins.get(key);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica expiração por inatividade (TTL)
|
||||||
|
if (Instant.now().isAfter(entry.lastAccessedAt().plus(SESSION_TTL_MINUTES, ChronoUnit.MINUTES))) {
|
||||||
|
clearPin(applicationId, certId);
|
||||||
|
logger.info("PIN da sessão expirou por inatividade (TTL {} min) para app '{}' e cert '{}'.", SESSION_TTL_MINUTES, applicationId, certId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza timestamp de último acesso
|
||||||
|
sessionPins.put(key, new PinEntry(entry.pin(), Instant.now()));
|
||||||
|
return entry.pin().clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica se existe um PIN válido salvo nesta sessão para o certificado.
|
* Recupera o PIN no escopo padrão (compatibilidade).
|
||||||
|
*/
|
||||||
|
public synchronized char[] getPin(String certId) {
|
||||||
|
return getPin("default", certId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se existe um PIN válido salvo nesta sessão para a aplicação e certificado.
|
||||||
|
*/
|
||||||
|
public synchronized boolean hasPin(String applicationId, String certId) {
|
||||||
|
return getPin(applicationId, certId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se existe um PIN no escopo padrão (compatibilidade).
|
||||||
*/
|
*/
|
||||||
public synchronized boolean hasPin(String certId) {
|
public synchronized boolean hasPin(String certId) {
|
||||||
if (certId == null || alwaysPromptPin) {
|
return hasPin("default", certId);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return sessionPins.containsKey(certId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove o PIN salvo para um certificado específico, zerando a memória.
|
* Remove o PIN salvo para uma aplicação e certificado específicos, zerando a memória.
|
||||||
*/
|
*/
|
||||||
public synchronized void clearPin(String certId) {
|
public synchronized void clearPin(String applicationId, String certId) {
|
||||||
if (certId != null) {
|
if (certId != null) {
|
||||||
char[] old = sessionPins.remove(certId);
|
String key = buildKey(applicationId, certId);
|
||||||
if (old != null) {
|
PinEntry old = sessionPins.remove(key);
|
||||||
Arrays.fill(old, ' ');
|
if (old != null && old.pin() != null) {
|
||||||
|
Arrays.fill(old.pin(), ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove o PIN no escopo padrão (compatibilidade).
|
||||||
|
*/
|
||||||
|
public synchronized void clearPin(String certId) {
|
||||||
|
clearPin("default", certId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpa imediatamente todos os PINs em memória e zera seus caracteres.
|
* Limpa imediatamente todos os PINs em memória e zera seus caracteres.
|
||||||
*/
|
*/
|
||||||
public synchronized void clearAll() {
|
public synchronized void clearAll() {
|
||||||
for (char[] pin : sessionPins.values()) {
|
for (PinEntry entry : sessionPins.values()) {
|
||||||
if (pin != null) {
|
if (entry != null && entry.pin() != null) {
|
||||||
Arrays.fill(pin, ' ');
|
Arrays.fill(entry.pin(), ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionPins.clear();
|
sessionPins.clear();
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import javax.crypto.SecretKeyFactory;
|
|||||||
import javax.crypto.spec.GCMParameterSpec;
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.spec.KeySpec;
|
import java.security.spec.KeySpec;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -24,13 +24,14 @@ import java.util.Base64;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
|
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
|
||||||
* <p>
|
* <p>
|
||||||
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
|
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
|
||||||
* a partir de atributos do usuário e da máquina local (PBKDF2WithHmacSHA256).
|
* a partir de semente aleatória protegida no disco local do usuário e atributos estáveis da máquina (PBKDF2WithHmacSHA256).
|
||||||
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
|
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
|
||||||
*/
|
*/
|
||||||
public final class SecurePinStorage {
|
public final class SecurePinStorage {
|
||||||
@@ -39,6 +40,7 @@ public final class SecurePinStorage {
|
|||||||
|
|
||||||
private static final String FILE_NAME = "secure-pins.dat";
|
private static final String FILE_NAME = "secure-pins.dat";
|
||||||
private static final String SALT_FILE_NAME = "secure-pins.salt";
|
private static final String SALT_FILE_NAME = "secure-pins.salt";
|
||||||
|
private static final String SEED_FILE_NAME = "secure-pins.seed";
|
||||||
private static final int GCM_IV_LENGTH = 12;
|
private static final int GCM_IV_LENGTH = 12;
|
||||||
private static final int GCM_TAG_LENGTH = 128;
|
private static final int GCM_TAG_LENGTH = 128;
|
||||||
private static final int PBKDF2_ITERATIONS = 65536;
|
private static final int PBKDF2_ITERATIONS = 65536;
|
||||||
@@ -49,6 +51,7 @@ public final class SecurePinStorage {
|
|||||||
private final Path storageDirectory;
|
private final Path storageDirectory;
|
||||||
private final Path dataFile;
|
private final Path dataFile;
|
||||||
private final Path saltFile;
|
private final Path saltFile;
|
||||||
|
private final Path seedFile;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
|
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@@ -64,6 +67,7 @@ public final class SecurePinStorage {
|
|||||||
this.storageDirectory = Objects.requireNonNull(storageDirectory);
|
this.storageDirectory = Objects.requireNonNull(storageDirectory);
|
||||||
this.dataFile = storageDirectory.resolve(FILE_NAME);
|
this.dataFile = storageDirectory.resolve(FILE_NAME);
|
||||||
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
|
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
|
||||||
|
this.seedFile = storageDirectory.resolve(SEED_FILE_NAME);
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
loadFromDisk();
|
loadFromDisk();
|
||||||
}
|
}
|
||||||
@@ -200,6 +204,7 @@ public final class SecurePinStorage {
|
|||||||
fos.write(iv);
|
fos.write(iv);
|
||||||
fos.write(ciphertext);
|
fos.write(ciphertext);
|
||||||
}
|
}
|
||||||
|
restrictFilePermissions(dataFile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
|
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@@ -248,22 +253,56 @@ public final class SecurePinStorage {
|
|||||||
byte[] salt = new byte[32];
|
byte[] salt = new byte[32];
|
||||||
new SecureRandom().nextBytes(salt);
|
new SecureRandom().nextBytes(salt);
|
||||||
Files.write(saltFile, salt);
|
Files.write(saltFile, salt);
|
||||||
|
restrictFilePermissions(saltFile);
|
||||||
return salt;
|
return salt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] getOrCreateSeed() throws Exception {
|
||||||
|
if (Files.exists(seedFile)) {
|
||||||
|
return Files.readAllBytes(seedFile);
|
||||||
|
}
|
||||||
|
byte[] seed = new byte[32];
|
||||||
|
new SecureRandom().nextBytes(seed);
|
||||||
|
Files.write(seedFile, seed);
|
||||||
|
restrictFilePermissions(seedFile);
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restrictFilePermissions(Path path) {
|
||||||
|
try {
|
||||||
|
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
|
||||||
|
if (view != null) {
|
||||||
|
Files.setPosixFilePermissions(path, Set.of(
|
||||||
|
PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_WRITE
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Em sistemas não-POSIX como Windows, as permissões são herdadas da pasta de perfil do usuário (~/.cybercore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SecretKey deriveKey(byte[] salt) throws Exception {
|
private SecretKey deriveKey(byte[] salt) throws Exception {
|
||||||
// Combina identificadores estáveis exclusivos da máquina e usuário
|
byte[] seed = getOrCreateSeed();
|
||||||
String hostIdentity = String.join(":",
|
String hostIdentity = String.join(":",
|
||||||
System.getProperty("user.name", "unknown"),
|
System.getProperty("user.name", "unknown"),
|
||||||
System.getProperty("user.home", "unknown"),
|
System.getProperty("user.home", "unknown"),
|
||||||
System.getProperty("os.name", "unknown"),
|
System.getProperty("os.name", "unknown"),
|
||||||
System.getProperty("os.arch", "unknown"),
|
System.getProperty("os.arch", "unknown")
|
||||||
"cybercore-signer-hardware-pin-binding"
|
|
||||||
);
|
);
|
||||||
|
byte[] hostBytes = hostIdentity.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] combinedMaterial = new byte[seed.length + hostBytes.length];
|
||||||
|
System.arraycopy(seed, 0, combinedMaterial, 0, seed.length);
|
||||||
|
System.arraycopy(hostBytes, 0, combinedMaterial, seed.length, hostBytes.length);
|
||||||
|
|
||||||
KeySpec spec = new PBEKeySpec(hostIdentity.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
|
char[] materialChars = Base64.getEncoder().encodeToString(combinedMaterial).toCharArray();
|
||||||
|
KeySpec spec = new PBEKeySpec(materialChars, salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
|
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
|
||||||
|
|
||||||
|
Arrays.fill(materialChars, ' ');
|
||||||
|
Arrays.fill(combinedMaterial, (byte) 0);
|
||||||
|
|
||||||
return new SecretKeySpec(keyBytes, "AES");
|
return new SecretKeySpec(keyBytes, "AES");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ public class SmartCardWatcher {
|
|||||||
void onCardRemoved(String terminalName);
|
void onCardRemoved(String terminalName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final long FAST_POLL_TIMEOUT_MS = 1500L;
|
||||||
|
private static final long MAX_POLL_TIMEOUT_MS = 5000L;
|
||||||
|
private static final int IDLE_CYCLES_BEFORE_BACKOFF = 4;
|
||||||
|
|
||||||
private final List<CardEventListener> listeners = new CopyOnWriteArrayList<>();
|
private final List<CardEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
private final Set<String> knownCards = Collections.synchronizedSet(new HashSet<>());
|
private final Set<String> knownCards = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
@@ -109,14 +113,21 @@ public class SmartCardWatcher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long currentTimeout = FAST_POLL_TIMEOUT_MS;
|
||||||
|
int consecutiveIdleCycles = 0;
|
||||||
|
|
||||||
while (running) {
|
while (running) {
|
||||||
try {
|
try {
|
||||||
// Aguarda alteração de estado no hardware (timeout de 1.5s para checar flag running)
|
// Aguarda alteração de estado no hardware com timeout adaptativo para reduzir desperdício de CPU
|
||||||
Boolean changed = (Boolean) waitForChangeMethod.invoke(cardTerminals, 1500L);
|
Boolean changed = (Boolean) waitForChangeMethod.invoke(cardTerminals, currentTimeout);
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(changed) || !running) {
|
if (Boolean.TRUE.equals(changed) || !running) {
|
||||||
if (!running) break;
|
if (!running) break;
|
||||||
|
|
||||||
|
// Restabelece polling rápido imediatamente ao registrar atividade no hardware
|
||||||
|
currentTimeout = FAST_POLL_TIMEOUT_MS;
|
||||||
|
consecutiveIdleCycles = 0;
|
||||||
|
|
||||||
Set<String> current = getCurrentTerminalsWithCards(cardTerminals, listMethod, cardPresentState);
|
Set<String> current = getCurrentTerminalsWithCards(cardTerminals, listMethod, cardPresentState);
|
||||||
|
|
||||||
// Dispositivos removidos
|
// Dispositivos removidos
|
||||||
@@ -140,6 +151,12 @@ public class SmartCardWatcher {
|
|||||||
|
|
||||||
knownCards.clear();
|
knownCards.clear();
|
||||||
knownCards.addAll(current);
|
knownCards.addAll(current);
|
||||||
|
} else {
|
||||||
|
// Nenhuma alteração no ciclo: aumenta gradualmente o intervalo de espera para economizar CPU
|
||||||
|
consecutiveIdleCycles++;
|
||||||
|
if (consecutiveIdleCycles >= IDLE_CYCLES_BEFORE_BACKOFF) {
|
||||||
|
currentTimeout = Math.min(MAX_POLL_TIMEOUT_MS, currentTimeout + 1000L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (e instanceof InterruptedException || (e.getCause() instanceof InterruptedException)) {
|
if (e instanceof InterruptedException || (e.getCause() instanceof InterruptedException)) {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package br.com.cybercore.signer.core.validation;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.cert.CertificateExpiredException;
|
||||||
|
import java.security.cert.CertificateNotYetValidException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validador rigoroso de cadeias de certificados digitais X.509 ICP-Brasil e padrões internacionais.
|
||||||
|
* Realiza validação temporal, integridade de assinaturas hierárquicas, restrições básicas (CA)
|
||||||
|
* e ancoragem em raízes confiáveis (TrustStore), com mapeamento preciso para erros do protocolo CCSP.
|
||||||
|
*/
|
||||||
|
public final class CertificateChainValidator {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CertificateChainValidator.class);
|
||||||
|
|
||||||
|
private CertificateChainValidator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida a cadeia de certificados informada em relação à data/hora atual.
|
||||||
|
*/
|
||||||
|
public static void validateChain(List<X509Certificate> chain, Set<X509Certificate> trustedRoots)
|
||||||
|
throws CertificateProviderException {
|
||||||
|
validateChain(chain, trustedRoots, Date.from(Instant.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida a cadeia de certificados informada em relação à data/hora de referência especificada.
|
||||||
|
*/
|
||||||
|
public static void validateChain(List<X509Certificate> chain, Set<X509Certificate> trustedRoots, Date referenceDate)
|
||||||
|
throws CertificateProviderException {
|
||||||
|
|
||||||
|
if (chain == null || chain.isEmpty()) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_NOT_FOUND,
|
||||||
|
"Cadeia de certificados vazia ou não informada."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Date dateToValidate = (referenceDate != null) ? referenceDate : Date.from(Instant.now());
|
||||||
|
|
||||||
|
// 1. Validação temporal de cada elo da cadeia
|
||||||
|
for (X509Certificate cert : chain) {
|
||||||
|
try {
|
||||||
|
cert.checkValidity(dateToValidate);
|
||||||
|
} catch (CertificateExpiredException e) {
|
||||||
|
logger.warn("Certificado expirado na cadeia: {}", cert.getSubjectX500Principal());
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_EXPIRED,
|
||||||
|
"Certificado '" + cert.getSubjectX500Principal().getName() + "' está expirado em " + dateToValidate,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
} catch (CertificateNotYetValidException e) {
|
||||||
|
logger.warn("Certificado ainda não válido na cadeia: {}", cert.getSubjectX500Principal());
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_INVALID,
|
||||||
|
"Certificado '" + cert.getSubjectX500Principal().getName() + "' ainda não é válido em " + dateToValidate,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validação hierárquica de assinaturas e restrições básicas de CA
|
||||||
|
for (int i = 0; i < chain.size() - 1; i++) {
|
||||||
|
X509Certificate subjectCert = chain.get(i);
|
||||||
|
X509Certificate issuerCert = chain.get(i + 1);
|
||||||
|
|
||||||
|
// Verifica se o emissor do subject é de fato o issuer
|
||||||
|
if (!subjectCert.getIssuerX500Principal().equals(issuerCert.getSubjectX500Principal())) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_INVALID,
|
||||||
|
"Quebra na cadeia de confiança: emissor de '" + subjectCert.getSubjectX500Principal().getName()
|
||||||
|
+ "' não corresponde ao sujeito de '" + issuerCert.getSubjectX500Principal().getName() + "'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// O emissor deve ser uma autoridade certificadora (BasicConstraints isCA == true)
|
||||||
|
if (issuerCert.getBasicConstraints() < 0) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_INVALID,
|
||||||
|
"O certificado intermediário '" + issuerCert.getSubjectX500Principal().getName()
|
||||||
|
+ "' não possui extensão BasicConstraints autorizando emissão de certificados (isCA=false)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica a assinatura criptográfica de subjectCert pela chave pública de issuerCert
|
||||||
|
try {
|
||||||
|
subjectCert.verify(issuerCert.getPublicKey());
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
logger.error("Assinatura inválida no certificado {}: {}", subjectCert.getSubjectX500Principal(), e.getMessage());
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_INVALID,
|
||||||
|
"A assinatura do certificado '" + subjectCert.getSubjectX500Principal().getName()
|
||||||
|
+ "' não pôde ser verificada pela chave pública de seu emissor: " + e.getMessage(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validação da raiz (Root CA)
|
||||||
|
X509Certificate rootCert = chain.get(chain.size() - 1);
|
||||||
|
boolean isSelfSigned = rootCert.getIssuerX500Principal().equals(rootCert.getSubjectX500Principal());
|
||||||
|
|
||||||
|
if (isSelfSigned) {
|
||||||
|
try {
|
||||||
|
rootCert.verify(rootCert.getPublicKey());
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_INVALID,
|
||||||
|
"Assinatura do certificado raiz autoassinado é inválida.",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação contra TrustStore de raízes confiáveis se fornecido
|
||||||
|
if (trustedRoots != null && !trustedRoots.isEmpty()) {
|
||||||
|
boolean anchoredInTrustStore = false;
|
||||||
|
for (X509Certificate trusted : trustedRoots) {
|
||||||
|
if (trusted.equals(rootCert) || (isSelfSigned && trusted.getPublicKey().equals(rootCert.getPublicKey()))) {
|
||||||
|
anchoredInTrustStore = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anchoredInTrustStore) {
|
||||||
|
throw new CertificateProviderException(
|
||||||
|
CcspErrorCode.CERTIFICATE_INVALID,
|
||||||
|
"A Autoridade Certificadora Raiz '" + rootCert.getSubjectX500Principal().getName()
|
||||||
|
+ "' não está presente na lista de raízes confiáveis do sistema."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package br.com.cybercore.signer.core.compliance;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSealPosition;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSigningService;
|
||||||
|
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||||
|
import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider;
|
||||||
|
import br.com.cybercore.signer.core.validation.SignatureValidationService;
|
||||||
|
import br.com.cybercore.signer.core.validation.ValidationResult;
|
||||||
|
import br.com.cybercore.signer.core.validation.ValidationStatus;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSignatureProfile;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSigningService;
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
|
import org.bouncycastle.asn1.ASN1EncodableVector;
|
||||||
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
|
import org.bouncycastle.asn1.DEROctetString;
|
||||||
|
import org.bouncycastle.asn1.DERSequence;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||||
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralName;
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||||
|
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
|
import org.bouncycastle.cms.SignerInformation;
|
||||||
|
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.xml.crypto.dsig.XMLSignature;
|
||||||
|
import javax.xml.crypto.dsig.XMLSignatureFactory;
|
||||||
|
import javax.xml.crypto.dsig.dom.DOMValidateContext;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suíte de testes automatizados de conformidade criptográfica para assinaturas
|
||||||
|
* PAdES (PDF) e XAdES-BES / XMLDSig (XML), utilizando cadeia ICP-Brasil de teste gerada em memória.
|
||||||
|
*/
|
||||||
|
@DisplayName("Suíte de Testes de Conformidade ICP-Brasil (PAdES e XAdES-BES)")
|
||||||
|
class ComplianceSignatureSuiteTest {
|
||||||
|
|
||||||
|
private static CertificateManager certificateManager;
|
||||||
|
private static PdfSigningService pdfSigningService;
|
||||||
|
private static XmlSigningService xmlSigningService;
|
||||||
|
private static SignatureValidationService validationService;
|
||||||
|
|
||||||
|
private static String testEcpfCertId;
|
||||||
|
private static X509Certificate rootCaCert;
|
||||||
|
private static X509Certificate intermediateCaCert;
|
||||||
|
private static X509Certificate signerCert;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initTestingCaAndServices() throws Exception {
|
||||||
|
certificateManager = new CertificateManager();
|
||||||
|
validationService = new SignatureValidationService();
|
||||||
|
|
||||||
|
// 1. Gera par de chaves da AC Raiz ICP-Brasil de Teste
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
KeyPair rootKeyPair = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Date notBefore = new Date(now - 100_000);
|
||||||
|
Date notAfter = new Date(now + 365L * 24 * 3600 * 1000);
|
||||||
|
|
||||||
|
X500Name rootSubject = new X500Name("CN=Autoridade Certificadora Raiz ICP-Brasil Teste v1, OU=Testes Criptograficos, O=ICP-Brasil, C=BR");
|
||||||
|
X509v3CertificateBuilder rootBuilder = new JcaX509v3CertificateBuilder(
|
||||||
|
rootSubject,
|
||||||
|
BigInteger.valueOf(1),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
rootSubject,
|
||||||
|
rootKeyPair.getPublic()
|
||||||
|
);
|
||||||
|
rootBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
|
||||||
|
rootBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
|
||||||
|
|
||||||
|
ContentSigner rootSigner = new JcaContentSignerBuilder("SHA256withRSA").build(rootKeyPair.getPrivate());
|
||||||
|
rootCaCert = new JcaX509CertificateConverter().getCertificate(rootBuilder.build(rootSigner));
|
||||||
|
|
||||||
|
// 2. Gera AC Intermediária de Teste assinada pela Raiz
|
||||||
|
KeyPair intermediateKeyPair = kpg.generateKeyPair();
|
||||||
|
X500Name intermediateSubject = new X500Name("CN=AC Autoridade Certificadora Intermediaria Teste, OU=Testes ICP-Brasil, O=ICP-Brasil, C=BR");
|
||||||
|
X509v3CertificateBuilder interBuilder = new JcaX509v3CertificateBuilder(
|
||||||
|
rootSubject,
|
||||||
|
BigInteger.valueOf(2),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
intermediateSubject,
|
||||||
|
intermediateKeyPair.getPublic()
|
||||||
|
);
|
||||||
|
interBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
|
||||||
|
interBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
|
||||||
|
|
||||||
|
ContentSigner interSigner = new JcaContentSignerBuilder("SHA256withRSA").build(rootKeyPair.getPrivate());
|
||||||
|
intermediateCaCert = new JcaX509CertificateConverter().getCertificate(interBuilder.build(interSigner));
|
||||||
|
|
||||||
|
// 3. Gera Certificado de Usuário Final (e-CPF com OID ICP-Brasil 2.16.76.1.3.1)
|
||||||
|
KeyPair userKeyPair = kpg.generateKeyPair();
|
||||||
|
X500Name userSubject = new X500Name("CN=CARLOS EDUARDO DA SILVA:12345678900, OU=e-CPF Brasil, O=ICP-Brasil, C=BR");
|
||||||
|
X509v3CertificateBuilder userBuilder = new JcaX509v3CertificateBuilder(
|
||||||
|
intermediateSubject,
|
||||||
|
BigInteger.valueOf(3),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
userSubject,
|
||||||
|
userKeyPair.getPublic()
|
||||||
|
);
|
||||||
|
userBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
|
||||||
|
userBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation));
|
||||||
|
|
||||||
|
// Adiciona extensão SubjectAlternativeName com OtherName contendo OID ICP-Brasil 2.16.76.1.3.1
|
||||||
|
ASN1EncodableVector ecpfVector = new ASN1EncodableVector();
|
||||||
|
ecpfVector.add(new ASN1ObjectIdentifier("2.16.76.1.3.1"));
|
||||||
|
ecpfVector.add(new DEROctetString("01011990123456789001234567890123456789012345678".getBytes(StandardCharsets.UTF_8)));
|
||||||
|
GeneralName otherName = new GeneralName(GeneralName.otherName, new DERSequence(ecpfVector));
|
||||||
|
userBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(otherName));
|
||||||
|
|
||||||
|
ContentSigner userSigner = new JcaContentSignerBuilder("SHA256withRSA").build(intermediateKeyPair.getPrivate());
|
||||||
|
signerCert = new JcaX509CertificateConverter().getCertificate(userBuilder.build(userSigner));
|
||||||
|
|
||||||
|
// Carrega em KeyStore PKCS12 em memória com a cadeia completa
|
||||||
|
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||||
|
ks.load(null, null);
|
||||||
|
char[] pwd = "test-compliance-pwd".toCharArray();
|
||||||
|
ks.setKeyEntry("ecpf-compliance-cert", userKeyPair.getPrivate(), pwd, new Certificate[]{signerCert, intermediateCaCert, rootCaCert});
|
||||||
|
|
||||||
|
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("compliance-p12-provider", "Provedor Compliance Teste");
|
||||||
|
provider.setKeyStore(ks, pwd);
|
||||||
|
certificateManager.registerProvider(provider);
|
||||||
|
|
||||||
|
List<ManagedCertificate> certs = certificateManager.discoverCertificates();
|
||||||
|
testEcpfCertId = certs.get(0).id();
|
||||||
|
|
||||||
|
pdfSigningService = new PdfSigningService(certificateManager);
|
||||||
|
xmlSigningService = new XmlSigningService(certificateManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("PAdES: Deve gerar documento PDF assinado com envelope CMS desacoplado e validar conformidade estrutural")
|
||||||
|
void shouldGenerateAndValidateCompliantPadesSignature() throws Exception {
|
||||||
|
// Cria documento PDF em memória
|
||||||
|
byte[] originalPdf;
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage page = new PDPage();
|
||||||
|
doc.addPage(page);
|
||||||
|
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new org.apache.pdfbox.pdmodel.font.PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 700);
|
||||||
|
cs.showText("CONTRATO DE PRESTACAO DE SERVICOS - COMPLIANCE TEST");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
originalPdf = baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assina via PdfSigningService
|
||||||
|
PdfSignatureOptions options = PdfSignatureOptions.builder(testEcpfCertId)
|
||||||
|
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.reason("Aprovacao Contratual")
|
||||||
|
.location("Brasilia, DF")
|
||||||
|
.build();
|
||||||
|
byte[] signedPdf = pdfSigningService.signPdf(originalPdf, options);
|
||||||
|
|
||||||
|
assertThat(signedPdf).isNotEmpty();
|
||||||
|
assertThat(signedPdf.length).isGreaterThan(originalPdf.length);
|
||||||
|
|
||||||
|
// 1. Validação independente via Apache PDFBox
|
||||||
|
try (PDDocument signedDoc = Loader.loadPDF(signedPdf)) {
|
||||||
|
List<PDSignature> signatures = signedDoc.getSignatureDictionaries();
|
||||||
|
assertThat(signatures).hasSize(1);
|
||||||
|
|
||||||
|
PDSignature sig = signatures.get(0);
|
||||||
|
assertThat(sig.getFilter()).isEqualTo("Adobe.PPKLite");
|
||||||
|
assertThat(sig.getSubFilter()).isEqualTo("adbe.pkcs7.detached");
|
||||||
|
assertThat(sig.getReason()).isEqualTo("Aprovacao Contratual");
|
||||||
|
assertThat(sig.getLocation()).isEqualTo("Brasilia, DF");
|
||||||
|
|
||||||
|
int[] byteRange = sig.getByteRange();
|
||||||
|
assertThat(byteRange).hasSize(4);
|
||||||
|
assertThat(byteRange[0]).isEqualTo(0);
|
||||||
|
assertThat(byteRange[1] + byteRange[3]).isEqualTo(signedPdf.length - (byteRange[2] - byteRange[1]));
|
||||||
|
|
||||||
|
// 2. Validação criptográfica do envelope CMS/PKCS#7 com Bouncy Castle independente
|
||||||
|
byte[] signedContentBytes = sig.getSignedContent(signedPdf);
|
||||||
|
byte[] cmsBytes = sig.getContents(signedPdf);
|
||||||
|
CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(signedContentBytes), cmsBytes);
|
||||||
|
|
||||||
|
assertThat(signedData.getSignerInfos().getSigners()).hasSize(1);
|
||||||
|
SignerInformation signerInfo = signedData.getSignerInfos().getSigners().iterator().next();
|
||||||
|
|
||||||
|
// Verifica se a assinatura bate com a chave pública do certificado do signatário
|
||||||
|
boolean verified = signerInfo.verify(new JcaSimpleSignerInfoVerifierBuilder().build(signerCert));
|
||||||
|
assertThat(verified).isTrue();
|
||||||
|
|
||||||
|
// Verifica se o digest embutido no CMS corresponde ao digest calculado dos bytes do documento
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] calculatedDigest = md.digest(signedContentBytes);
|
||||||
|
|
||||||
|
byte[] messageDigestAttr = signerInfo.getSignedAttributes()
|
||||||
|
.get(org.bouncycastle.asn1.cms.CMSAttributes.messageDigest)
|
||||||
|
.getAttrValues().getObjectAt(0).toASN1Primitive().getEncoded();
|
||||||
|
// Atributo ASN1 DER OctetString contém o digest
|
||||||
|
assertThat(messageDigestAttr).contains(calculatedDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validação através do SignatureValidationService do CyberCore
|
||||||
|
ValidationResult valResult = validationService.validateAttached(signedPdf, "documento_assinado.pdf");
|
||||||
|
assertThat(valResult.status()).isEqualTo(ValidationStatus.VALID);
|
||||||
|
assertThat(valResult.signers()).hasSize(1);
|
||||||
|
assertThat(valResult.signers().get(0).signerName()).contains("CARLOS EDUARDO DA SILVA");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("XAdES-BES: Deve gerar documento XML assinado com propriedades qualificadas e validar via JSR-105")
|
||||||
|
void shouldGenerateAndValidateCompliantXadesBesSignature() throws Exception {
|
||||||
|
String originalXml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<declaracao id="dec-9988">
|
||||||
|
<titular>Carlos Eduardo da Silva</titular>
|
||||||
|
<cpf>12345678900</cpf>
|
||||||
|
<cidade>Brasilia</cidade>
|
||||||
|
</declaracao>
|
||||||
|
""";
|
||||||
|
|
||||||
|
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testEcpfCertId, null, XmlSignatureProfile.XADES_BES);
|
||||||
|
|
||||||
|
byte[] signedXmlBytes = xmlSigningService.signXml(originalXml.getBytes(StandardCharsets.UTF_8), options);
|
||||||
|
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// 1. Verificação de tags obrigatórias do XAdES-BES (ETSI TS 101 903 e DOC-ICP-15.03)
|
||||||
|
assertThat(signedXmlStr).contains("<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"");
|
||||||
|
assertThat(signedXmlStr).contains("<xades:QualifyingProperties");
|
||||||
|
assertThat(signedXmlStr).contains("<xades:SignedProperties");
|
||||||
|
assertThat(signedXmlStr).contains("<xades:SigningTime");
|
||||||
|
assertThat(signedXmlStr).contains("<xades:SigningCertificate");
|
||||||
|
assertThat(signedXmlStr).contains("http://uri.etsi.org/01903#SignedProperties");
|
||||||
|
|
||||||
|
// 2. Validação criptográfica independente via API JSR-105 (DOMValidateContext)
|
||||||
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
|
dbf.setNamespaceAware(true);
|
||||||
|
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
|
||||||
|
|
||||||
|
NodeList sigNodes = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||||
|
assertThat(sigNodes.getLength()).isEqualTo(1);
|
||||||
|
|
||||||
|
DOMValidateContext valContext = new DOMValidateContext(signerCert.getPublicKey(), sigNodes.item(0));
|
||||||
|
NodeList allNodes = doc.getElementsByTagName("*");
|
||||||
|
for (int n = 0; n < allNodes.getLength(); n++) {
|
||||||
|
if (allNodes.item(n) instanceof Element e) {
|
||||||
|
if (e.hasAttribute("Id")) {
|
||||||
|
valContext.setIdAttributeNS(e, null, "Id");
|
||||||
|
}
|
||||||
|
if (e.hasAttribute("id")) {
|
||||||
|
valContext.setIdAttributeNS(e, null, "id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM");
|
||||||
|
XMLSignature xmlSignature = sigFactory.unmarshalXMLSignature(valContext);
|
||||||
|
|
||||||
|
boolean coreValidity = xmlSignature.validate(valContext);
|
||||||
|
assertThat(coreValidity).isTrue();
|
||||||
|
|
||||||
|
// 3. Validação pós-adulteração: se alterar qualquer caractere do XML original, a assinatura deve falhar
|
||||||
|
byte[] tamperedXml = signedXmlStr.replace("Carlos Eduardo da Silva", "Atacante Malicioso").getBytes(StandardCharsets.UTF_8);
|
||||||
|
Document tamperedDoc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(tamperedXml));
|
||||||
|
NodeList tamperedSigNodes = tamperedDoc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||||
|
DOMValidateContext tamperedContext = new DOMValidateContext(signerCert.getPublicKey(), tamperedSigNodes.item(0));
|
||||||
|
NodeList allTamperedNodes = tamperedDoc.getElementsByTagName("*");
|
||||||
|
for (int n = 0; n < allTamperedNodes.getLength(); n++) {
|
||||||
|
if (allTamperedNodes.item(n) instanceof Element e) {
|
||||||
|
if (e.hasAttribute("Id")) {
|
||||||
|
tamperedContext.setIdAttributeNS(e, null, "Id");
|
||||||
|
}
|
||||||
|
if (e.hasAttribute("id")) {
|
||||||
|
tamperedContext.setIdAttributeNS(e, null, "id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
XMLSignature tamperedSig = sigFactory.unmarshalXMLSignature(tamperedContext);
|
||||||
|
|
||||||
|
boolean tamperedValidity = tamperedSig.validate(tamperedContext);
|
||||||
|
assertThat(tamperedValidity).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package br.com.cybercore.signer.core.crypto.revocation;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("Testes do RevocationCache com TTL")
|
||||||
|
class RevocationCacheTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve armazenar e recuperar valor válido dentro do TTL")
|
||||||
|
void shouldStoreAndRetrieveValidValue() {
|
||||||
|
RevocationCache<String, String> cache = new RevocationCache<>();
|
||||||
|
cache.put("cert-123", "VALID_OCSP_RESPONSE", Duration.ofMinutes(10));
|
||||||
|
|
||||||
|
Optional<String> result = cache.get("cert-123");
|
||||||
|
assertThat(result).isPresent().contains("VALID_OCSP_RESPONSE");
|
||||||
|
assertThat(cache.size()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve descartar valor expirado após o TTL")
|
||||||
|
void shouldExpireValueAfterTtl() throws InterruptedException {
|
||||||
|
RevocationCache<String, String> cache = new RevocationCache<>();
|
||||||
|
cache.put("cert-exp", "TEMPORARY", Duration.ofMillis(20));
|
||||||
|
|
||||||
|
Thread.sleep(50);
|
||||||
|
|
||||||
|
Optional<String> result = cache.get("cert-exp");
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getOrFetch deve invocar o fetcher apenas uma vez enquanto o cache estiver válido")
|
||||||
|
void shouldInvokeFetcherOnlyOnceWhileValid() {
|
||||||
|
RevocationCache<String, Integer> cache = new RevocationCache<>();
|
||||||
|
AtomicInteger counter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
Integer firstCall = cache.getOrFetch("key", Duration.ofMinutes(5), counter::incrementAndGet);
|
||||||
|
Integer secondCall = cache.getOrFetch("key", Duration.ofMinutes(5), counter::incrementAndGet);
|
||||||
|
|
||||||
|
assertThat(firstCall).isEqualTo(1);
|
||||||
|
assertThat(secondCall).isEqualTo(1);
|
||||||
|
assertThat(counter.get()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("evictExpired e clear devem limpar as entradas corretamente")
|
||||||
|
void shouldEvictExpiredAndClearCorrectly() throws InterruptedException {
|
||||||
|
RevocationCache<String, String> cache = new RevocationCache<>();
|
||||||
|
cache.put("k1", "val1", Duration.ofMillis(20));
|
||||||
|
cache.put("k2", "val2", Duration.ofMinutes(10));
|
||||||
|
|
||||||
|
Thread.sleep(50);
|
||||||
|
cache.evictExpired();
|
||||||
|
|
||||||
|
assertThat(cache.get("k1")).isEmpty();
|
||||||
|
assertThat(cache.get("k2")).isPresent();
|
||||||
|
|
||||||
|
cache.clear();
|
||||||
|
assertThat(cache.size()).isZero();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package br.com.cybercore.signer.core.crypto.tsa;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
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.bouncycastle.tsp.TSPAlgorithms;
|
||||||
|
import org.bouncycastle.tsp.TimeStampRequest;
|
||||||
|
import org.bouncycastle.tsp.TimeStampResponse;
|
||||||
|
import org.bouncycastle.tsp.TimeStampResponseGenerator;
|
||||||
|
import org.bouncycastle.tsp.TimeStampToken;
|
||||||
|
import org.bouncycastle.tsp.TimeStampTokenGenerator;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DisplayName("Testes do Cliente Resiliente de Carimbo do Tempo (TSA RFC 3161)")
|
||||||
|
class TsaClientTest {
|
||||||
|
|
||||||
|
private static KeyPair tsaKeyPair;
|
||||||
|
private static X509Certificate tsaCert;
|
||||||
|
private static HttpServer primaryServer;
|
||||||
|
private static HttpServer secondaryServer;
|
||||||
|
private static String primaryUrl;
|
||||||
|
private static String secondaryUrl;
|
||||||
|
private static final AtomicInteger primaryRequestCount = new AtomicInteger(0);
|
||||||
|
private static final AtomicInteger secondaryRequestCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() throws Exception {
|
||||||
|
// Gera par de chaves e certificado para a TSA de testes
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
tsaKeyPair = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
X500Name subject = new X500Name("CN=Autoridade de Carimbo do Tempo de Teste, O=ICP-Brasil, C=BR");
|
||||||
|
BigInteger serial = BigInteger.valueOf(101);
|
||||||
|
Date notBefore = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
|
||||||
|
Date notAfter = Date.from(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||||
|
|
||||||
|
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(tsaKeyPair.getPublic().getEncoded());
|
||||||
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
|
||||||
|
subject, serial, notBefore, notAfter, subject, subPubKeyInfo
|
||||||
|
);
|
||||||
|
certBuilder.addExtension(
|
||||||
|
org.bouncycastle.asn1.x509.Extension.extendedKeyUsage,
|
||||||
|
true,
|
||||||
|
new org.bouncycastle.asn1.x509.ExtendedKeyUsage(org.bouncycastle.asn1.x509.KeyPurposeId.id_kp_timeStamping)
|
||||||
|
);
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(tsaKeyPair.getPrivate());
|
||||||
|
X509CertificateHolder holder = certBuilder.build(signer);
|
||||||
|
tsaCert = new JcaX509CertificateConverter().getCertificate(holder);
|
||||||
|
|
||||||
|
// Servidor primário
|
||||||
|
primaryServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
primaryServer.createContext("/tsa", exchange -> {
|
||||||
|
int count = primaryRequestCount.incrementAndGet();
|
||||||
|
String query = exchange.getRequestURI().getQuery();
|
||||||
|
if (query != null && query.contains("error")) {
|
||||||
|
exchange.sendResponseHeaders(503, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query != null && query.contains("flaky") && count == 1) {
|
||||||
|
// Primeira tentativa falha, segunda sucede
|
||||||
|
exchange.sendResponseHeaders(500, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (InputStream is = exchange.getRequestBody()) {
|
||||||
|
byte[] reqBytes = is.readAllBytes();
|
||||||
|
byte[] respBytes = generateTsaResponse(reqBytes);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/timestamp-reply");
|
||||||
|
exchange.sendResponseHeaders(200, respBytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(respBytes);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
exchange.sendResponseHeaders(500, -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
primaryServer.start();
|
||||||
|
primaryUrl = "http://127.0.0.1:" + primaryServer.getAddress().getPort() + "/tsa";
|
||||||
|
|
||||||
|
// Servidor secundário (Fallback)
|
||||||
|
secondaryServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
secondaryServer.createContext("/tsa", exchange -> {
|
||||||
|
secondaryRequestCount.incrementAndGet();
|
||||||
|
try (InputStream is = exchange.getRequestBody()) {
|
||||||
|
byte[] reqBytes = is.readAllBytes();
|
||||||
|
byte[] respBytes = generateTsaResponse(reqBytes);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/timestamp-reply");
|
||||||
|
exchange.sendResponseHeaders(200, respBytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(respBytes);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
exchange.sendResponseHeaders(500, -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
secondaryServer.start();
|
||||||
|
secondaryUrl = "http://127.0.0.1:" + secondaryServer.getAddress().getPort() + "/tsa";
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() {
|
||||||
|
if (primaryServer != null) primaryServer.stop(0);
|
||||||
|
if (secondaryServer != null) secondaryServer.stop(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateTsaResponse(byte[] requestBytes) throws Exception {
|
||||||
|
TimeStampRequest req = new TimeStampRequest(requestBytes);
|
||||||
|
org.bouncycastle.operator.DigestCalculatorProvider digCalcProv = new JcaDigestCalculatorProviderBuilder().build();
|
||||||
|
org.bouncycastle.operator.DigestCalculator digCalc = digCalcProv.get(new org.bouncycastle.asn1.x509.AlgorithmIdentifier(org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_sha256));
|
||||||
|
|
||||||
|
org.bouncycastle.cms.SignerInfoGenerator signerInfoGen = new JcaSignerInfoGeneratorBuilder(digCalcProv)
|
||||||
|
.build(new JcaContentSignerBuilder("SHA256withRSA").build(tsaKeyPair.getPrivate()), tsaCert);
|
||||||
|
|
||||||
|
TimeStampTokenGenerator tokenGen = new TimeStampTokenGenerator(
|
||||||
|
signerInfoGen,
|
||||||
|
digCalc,
|
||||||
|
new org.bouncycastle.asn1.ASN1ObjectIdentifier("2.16.76.1.4.1") // OID Política ACT ICP-Brasil
|
||||||
|
);
|
||||||
|
TimeStampResponseGenerator respGen = new TimeStampResponseGenerator(tokenGen, java.util.Collections.singleton(TSPAlgorithms.SHA256));
|
||||||
|
TimeStampResponse resp = respGen.generate(req, BigInteger.valueOf(System.currentTimeMillis()), new Date());
|
||||||
|
return resp.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve emitir carimbo de tempo RFC 3161 com sucesso no servidor primário")
|
||||||
|
void shouldObtainTimestampSuccessfullyFromPrimary() throws Exception {
|
||||||
|
primaryRequestCount.set(0);
|
||||||
|
TsaClient client = new TsaClient(List.of(primaryUrl));
|
||||||
|
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest("teste-tsa".getBytes(StandardCharsets.UTF_8));
|
||||||
|
TimeStampToken token = client.requestTimestamp(digest);
|
||||||
|
|
||||||
|
assertThat(token).isNotNull();
|
||||||
|
assertThat(token.getTimeStampInfo().getGenTime()).isNotNull();
|
||||||
|
assertThat(primaryRequestCount.get()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve realizar retentativa com backoff quando o servidor primário oscilar temporariamente")
|
||||||
|
void shouldRetryWithBackoffOnTemporaryFailure() throws Exception {
|
||||||
|
primaryRequestCount.set(0);
|
||||||
|
String flakyUrl = primaryUrl + "?flaky=true";
|
||||||
|
TsaClient client = new TsaClient(List.of(flakyUrl), Duration.ofSeconds(2), 2, Duration.ofMillis(50), null);
|
||||||
|
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest("teste-retry".getBytes(StandardCharsets.UTF_8));
|
||||||
|
TimeStampToken token = client.requestTimestamp(digest);
|
||||||
|
|
||||||
|
assertThat(token).isNotNull();
|
||||||
|
assertThat(primaryRequestCount.get()).isEqualTo(2); // 1ª falhou, 2ª sucedeu
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve fazer fallback transparente para o servidor secundário quando o primário estiver indisponível")
|
||||||
|
void shouldFallbackToSecondaryServerWhenPrimaryFails() throws Exception {
|
||||||
|
secondaryRequestCount.set(0);
|
||||||
|
String offlineUrl = primaryUrl + "?error=true";
|
||||||
|
TsaClient client = new TsaClient(List.of(offlineUrl, secondaryUrl), Duration.ofSeconds(2), 1, Duration.ofMillis(20), null);
|
||||||
|
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest("teste-fallback".getBytes(StandardCharsets.UTF_8));
|
||||||
|
TimeStampToken token = client.requestTimestamp(digest);
|
||||||
|
|
||||||
|
assertThat(token).isNotNull();
|
||||||
|
assertThat(secondaryRequestCount.get()).isGreaterThanOrEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve lançar COMMUNICATION_ERROR se todos os servidores falharem")
|
||||||
|
void shouldThrowCommunicationErrorWhenAllServersFail() throws Exception {
|
||||||
|
String offlineUrl1 = primaryUrl + "?error=true";
|
||||||
|
String offlineUrl2 = "http://127.0.0.1:54321/offline";
|
||||||
|
TsaClient client = new TsaClient(List.of(offlineUrl1, offlineUrl2), Duration.ofMillis(500), 0, Duration.ofMillis(10), null);
|
||||||
|
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest("teste-falha-total".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> client.requestTimestamp(digest))
|
||||||
|
.isInstanceOf(CertificateProviderException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
CertificateProviderException cpe = (CertificateProviderException) ex;
|
||||||
|
assertThat(cpe.getErrorCode()).isEqualTo(CcspErrorCode.COMMUNICATION_ERROR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -289,4 +289,118 @@ class PdfSigningServiceTest {
|
|||||||
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
|
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
|
||||||
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
|
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve assinar arquivo PDF via signPdfFile com arquivos de origem e destino distintos")
|
||||||
|
void shouldSignPdfFileWithDistinctSourceAndTarget() throws Exception {
|
||||||
|
java.io.File src = java.io.File.createTempFile("cybercore_src_", ".pdf");
|
||||||
|
java.io.File tgt = java.io.File.createTempFile("cybercore_tgt_", ".pdf");
|
||||||
|
try {
|
||||||
|
java.nio.file.Files.write(src.toPath(), createSamplePdf());
|
||||||
|
|
||||||
|
PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||||
|
.reason("Assinatura de Arquivo Físico")
|
||||||
|
.location("Brasília, DF")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
pdfSigningService.signPdfFile(src, tgt, options);
|
||||||
|
|
||||||
|
assertThat(tgt.exists()).isTrue();
|
||||||
|
assertThat(tgt.length()).isGreaterThan(src.length());
|
||||||
|
|
||||||
|
// Valida leitura de páginas e dimensões a partir de File
|
||||||
|
assertThat(PdfSigningService.getPageCount(src)).isEqualTo(1);
|
||||||
|
assertThat(PdfSigningService.getPageCount(tgt)).isEqualTo(1);
|
||||||
|
PdfSigningService.PageDimensions dims = PdfSigningService.getPageDimensions(tgt, 0);
|
||||||
|
assertThat(dims.width()).isGreaterThan(0);
|
||||||
|
assertThat(dims.height()).isGreaterThan(0);
|
||||||
|
|
||||||
|
// Validação criptográfica do arquivo resultante
|
||||||
|
byte[] signedBytes = java.nio.file.Files.readAllBytes(tgt.toPath());
|
||||||
|
SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
ValidationResult res = validationService.validateAttached(signedBytes, "arquivo_assinado.pdf");
|
||||||
|
assertThat(res.isValid()).isTrue();
|
||||||
|
} finally {
|
||||||
|
java.nio.file.Files.deleteIfExists(src.toPath());
|
||||||
|
java.nio.file.Files.deleteIfExists(tgt.toPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve assinar arquivo PDF in-place (mesmo arquivo de origem e destino)")
|
||||||
|
void shouldSignInPlaceWhenSourceAndTargetAreSameFile() throws Exception {
|
||||||
|
java.io.File file = java.io.File.createTempFile("cybercore_inplace_", ".pdf");
|
||||||
|
try {
|
||||||
|
byte[] sample = createSamplePdf();
|
||||||
|
java.nio.file.Files.write(file.toPath(), sample);
|
||||||
|
long initialSize = file.length();
|
||||||
|
|
||||||
|
PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId)
|
||||||
|
.visibleSeal(false)
|
||||||
|
.reason("Assinatura In-Place")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
pdfSigningService.signPdfFile(file, file, options);
|
||||||
|
|
||||||
|
assertThat(file.exists()).isTrue();
|
||||||
|
assertThat(file.length()).isGreaterThan(initialSize);
|
||||||
|
|
||||||
|
byte[] signedBytes = java.nio.file.Files.readAllBytes(file.toPath());
|
||||||
|
SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
ValidationResult res = validationService.validateAttached(signedBytes, "inplace.pdf");
|
||||||
|
assertThat(res.isValid()).isTrue();
|
||||||
|
} finally {
|
||||||
|
java.nio.file.Files.deleteIfExists(file.toPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve assinar PDF grande (> 2MB) acionando a estratégia de arquivo temporário")
|
||||||
|
void shouldSignLargePdfTriggeringTempFileOptimization() throws Exception {
|
||||||
|
// Cria PDF com buffer grande embutido para ultrapassar 2MB
|
||||||
|
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage page = new PDPage();
|
||||||
|
doc.addPage(page);
|
||||||
|
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 10);
|
||||||
|
cs.newLineAtOffset(50, 700);
|
||||||
|
cs.showText("PDF com carga útil volumosa para teste de threshold de streaming.");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
// Anexa stream arbitrário para elevar o tamanho acima de 2MB
|
||||||
|
org.apache.pdfbox.pdmodel.common.PDStream stream = new org.apache.pdfbox.pdmodel.common.PDStream(doc);
|
||||||
|
try (java.io.OutputStream os = stream.createOutputStream()) {
|
||||||
|
byte[] chunk = new byte[65536];
|
||||||
|
java.util.Arrays.fill(chunk, (byte) 'X');
|
||||||
|
for (int i = 0; i < 35; i++) { // ~2.2 MB
|
||||||
|
os.write(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc.getDocumentCatalog().getCOSObject().setItem(
|
||||||
|
org.apache.pdfbox.cos.COSName.getPDFName("LargePayload"), stream.getCOSObject()
|
||||||
|
);
|
||||||
|
doc.save(baos);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] largePdf = baos.toByteArray();
|
||||||
|
assertThat(largePdf.length).isGreaterThan(2 * 1024 * 1024);
|
||||||
|
|
||||||
|
PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||||
|
.reason("Assinatura de Arquivo Grande")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] signed = pdfSigningService.signPdf(largePdf, options);
|
||||||
|
assertThat(signed).isNotNull();
|
||||||
|
assertThat(signed.length).isGreaterThan(largePdf.length);
|
||||||
|
|
||||||
|
SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
ValidationResult res = validationService.validateAttached(signed, "large_signed.pdf");
|
||||||
|
assertThat(res.isValid()).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
package br.com.cybercore.signer.core.protocol;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.crypto.SigningService;
|
||||||
|
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.SigningKeyMaterial;
|
||||||
|
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateProvider;
|
||||||
|
import br.com.cybercore.signer.core.provider.ProviderType;
|
||||||
|
import br.com.cybercore.signer.core.security.ApplicationRegistry;
|
||||||
|
import br.com.cybercore.signer.core.security.AuthorizedApplication;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@DisplayName("Testes de Validação Cruzada contra o JSON Schema CCSP/1 (ccsp-v1.schema.json)")
|
||||||
|
class CcspSchemaValidationTest {
|
||||||
|
|
||||||
|
private CcspServer server;
|
||||||
|
private HttpClient httpClient;
|
||||||
|
private String baseUrl;
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
private JsonNode schemaRoot;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@TempDir Path tempDir) throws Exception {
|
||||||
|
objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// Localiza e carrega docs/ccsp-v1.schema.json
|
||||||
|
Path schemaPath = Path.of("../../docs/ccsp-v1.schema.json");
|
||||||
|
if (!Files.exists(schemaPath)) {
|
||||||
|
schemaPath = Path.of("docs/ccsp-v1.schema.json");
|
||||||
|
}
|
||||||
|
assertThat(schemaPath).exists();
|
||||||
|
schemaRoot = objectMapper.readTree(Files.readString(schemaPath, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
CertificateManager certManager = new CertificateManager();
|
||||||
|
SigningService signingService = mock(SigningService.class);
|
||||||
|
ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json"));
|
||||||
|
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
KeyPair kp = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Date notBefore = new Date(now - 100000);
|
||||||
|
Date notAfter = new Date(now + 365L * 24 * 3600 * 1000);
|
||||||
|
|
||||||
|
X500Name subject = new X500Name("CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR");
|
||||||
|
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||||
|
subject,
|
||||||
|
BigInteger.valueOf(System.currentTimeMillis()),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject,
|
||||||
|
kp.getPublic()
|
||||||
|
);
|
||||||
|
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
|
||||||
|
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));
|
||||||
|
|
||||||
|
appRegistry.register(new AuthorizedApplication(
|
||||||
|
"app-schema-test",
|
||||||
|
"Schema Validation Test App",
|
||||||
|
Set.of("http://localhost:3000", "http://127.0.0.1:3000"),
|
||||||
|
true,
|
||||||
|
Instant.now()
|
||||||
|
));
|
||||||
|
|
||||||
|
ManagedCertificate testCert = new ManagedCertificate(
|
||||||
|
"cert-schema-1",
|
||||||
|
"Certificado Schema Teste",
|
||||||
|
CertificateType.A1,
|
||||||
|
CertificateStatus.AVAILABLE,
|
||||||
|
"CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR",
|
||||||
|
"CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR",
|
||||||
|
"12345678",
|
||||||
|
"a1b2c3",
|
||||||
|
"d4e5f6",
|
||||||
|
Instant.now().minus(10, ChronoUnit.DAYS),
|
||||||
|
Instant.now().plus(300, ChronoUnit.DAYS),
|
||||||
|
"SHA256withRSA",
|
||||||
|
2048,
|
||||||
|
"Schema Test ICP-Brasil",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"provider-schema-test",
|
||||||
|
"Certificado Schema Teste"
|
||||||
|
);
|
||||||
|
|
||||||
|
CertificateProvider dummyProvider = new CertificateProvider() {
|
||||||
|
@Override public String getProviderId() { return "provider-schema-test"; }
|
||||||
|
@Override public String getDisplayName() { return "Provedor Teste Schema"; }
|
||||||
|
@Override public ProviderType getType() { return ProviderType.A1_FILE; }
|
||||||
|
@Override public boolean isAvailable() { return true; }
|
||||||
|
@Override public List<ManagedCertificate> listCertificates() { return List.of(testCert); }
|
||||||
|
@Override public SigningResult sign(SigningContext context) {
|
||||||
|
return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now());
|
||||||
|
}
|
||||||
|
@Override public Optional<SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) {
|
||||||
|
return Optional.of(new SigningKeyMaterial(kp.getPrivate(), new X509Certificate[]{cert}, null));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
certManager.registerProvider(dummyProvider);
|
||||||
|
certManager.discoverCertificates();
|
||||||
|
|
||||||
|
when(signingService.sign(any())).thenReturn(
|
||||||
|
new SigningResult("req-1", new byte[]{10, 20, 30}, "base64-cert", List.of("base64-cert"), "SHA256withRSA", Instant.now())
|
||||||
|
);
|
||||||
|
|
||||||
|
server = new CcspServer(0, certManager, signingService, appRegistry, new CcspUserPromptListener() {
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
|
||||||
|
return CompletableFuture.completedFuture(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) {
|
||||||
|
return CompletableFuture.completedFuture(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
int port = server.getPort();
|
||||||
|
baseUrl = "http://127.0.0.1:" + port;
|
||||||
|
httpClient = HttpClient.newHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
if (server != null) {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Garante integridade e completude do catálogo de ErrorCode no schema formal")
|
||||||
|
void shouldValidateErrorCodeCatalogInSchema() {
|
||||||
|
JsonNode errorCodeEnum = schemaRoot.path("definitions").path("ErrorCode").path("enum");
|
||||||
|
assertThat(errorCodeEnum.isArray()).isTrue();
|
||||||
|
|
||||||
|
Set<String> errorCodes = new HashSet<>();
|
||||||
|
errorCodeEnum.forEach(node -> errorCodes.add(node.asText()));
|
||||||
|
|
||||||
|
assertThat(errorCodes).contains(
|
||||||
|
"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",
|
||||||
|
"COMMUNICATION_ERROR",
|
||||||
|
"AGENT_NOT_RUNNING",
|
||||||
|
"TIMEOUT_ERROR",
|
||||||
|
"BAD_REQUEST"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Valida resposta do endpoint /ccsp/v1/status contra a definição StatusResponse")
|
||||||
|
void shouldValidateStatusEndpointResponseAgainstSchema() throws Exception {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/status"))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(response.statusCode()).isEqualTo(200);
|
||||||
|
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
validateAgainstDefinition(json, "StatusResponse");
|
||||||
|
|
||||||
|
assertThat(json.path("version").asText()).isEqualTo("1");
|
||||||
|
assertThat(json.path("status").asText()).isEqualTo("ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Valida resposta do endpoint /ccsp/v1/authorize contra a definição AuthorizeResponse")
|
||||||
|
void shouldValidateAuthorizeEndpointResponseAgainstSchema() throws Exception {
|
||||||
|
String payload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"application_id": "app-schema-test",
|
||||||
|
"name": "Schema Validation Test App"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/authorize"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "http://localhost:3000")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(response.statusCode()).isEqualTo(200);
|
||||||
|
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
validateAgainstDefinition(json, "AuthorizeResponse");
|
||||||
|
|
||||||
|
assertThat(json.path("version").asText()).isEqualTo("1");
|
||||||
|
assertThat(json.path("status").asText()).isEqualTo("authorized");
|
||||||
|
assertThat(json.path("application_id").asText()).isEqualTo("app-schema-test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Valida resposta do endpoint /ccsp/v1/certificates contra CertificatesResponse e CertificateDto")
|
||||||
|
void shouldValidateCertificatesEndpointResponseAgainstSchema() throws Exception {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/certificates?application_id=app-schema-test"))
|
||||||
|
.header("Origin", "http://localhost:3000")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(response.statusCode()).isEqualTo(200);
|
||||||
|
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
validateAgainstDefinition(json, "CertificatesResponse");
|
||||||
|
|
||||||
|
JsonNode certificates = json.path("certificates");
|
||||||
|
assertThat(certificates.isArray()).isTrue();
|
||||||
|
assertThat(certificates.size()).isGreaterThanOrEqualTo(1);
|
||||||
|
|
||||||
|
for (JsonNode certNode : certificates) {
|
||||||
|
validateAgainstDefinition(certNode, "CertificateItem");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Valida resposta do endpoint /ccsp/v1/sign contra a definição SignResponse")
|
||||||
|
void shouldValidateSignEndpointResponseAgainstSchema() throws Exception {
|
||||||
|
String payload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-schema-test",
|
||||||
|
"operation": "sign",
|
||||||
|
"algorithm": "SHA256withRSA",
|
||||||
|
"digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||||
|
"certificate_hint": "cert-schema-1",
|
||||||
|
"document_name": "documento_teste.txt",
|
||||||
|
"nonce": "%s",
|
||||||
|
"expires_at": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
UUID.randomUUID(),
|
||||||
|
Instant.now().plus(60, ChronoUnit.SECONDS).toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "http://localhost:3000")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(response.statusCode()).isEqualTo(200);
|
||||||
|
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
validateAgainstDefinition(json, "SignResponse");
|
||||||
|
|
||||||
|
assertThat(json.path("version").asText()).isEqualTo("1");
|
||||||
|
assertThat(json.path("status").asText()).isEqualTo("signed");
|
||||||
|
assertThat(json.path("signature").asText()).isNotEmpty();
|
||||||
|
assertThat(json.path("certificate").asText()).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Valida resposta de erro contra a definição ErrorResponse e ErrorCode catalog")
|
||||||
|
void shouldValidateErrorResponseAgainstSchema() throws Exception {
|
||||||
|
String payload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-schema-test",
|
||||||
|
"operation": "sign",
|
||||||
|
"algorithm": "SHA256withRSA",
|
||||||
|
"digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||||
|
"certificate_hint": "non-existent-cert-id",
|
||||||
|
"nonce": "%s",
|
||||||
|
"expires_at": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
UUID.randomUUID(),
|
||||||
|
Instant.now().plus(60, ChronoUnit.SECONDS).toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "http://localhost:3000")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(payload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(response.statusCode()).isGreaterThanOrEqualTo(400);
|
||||||
|
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
validateAgainstDefinition(json, "ErrorResponse");
|
||||||
|
|
||||||
|
String errorCode = json.path("error_code").asText();
|
||||||
|
JsonNode errorCodeEnum = schemaRoot.path("definitions").path("ErrorCode").path("enum");
|
||||||
|
List<String> validCodes = new ArrayList<>();
|
||||||
|
errorCodeEnum.forEach(node -> validCodes.add(node.asText()));
|
||||||
|
|
||||||
|
assertThat(validCodes).contains(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Valida resposta do endpoint /ccsp/v1/seals contra a definição SealsResponse")
|
||||||
|
void shouldValidateSealsEndpointResponseAgainstSchema() throws Exception {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/seals"))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(response.statusCode()).isEqualTo(200);
|
||||||
|
|
||||||
|
JsonNode json = objectMapper.readTree(response.body());
|
||||||
|
validateAgainstDefinition(json, "SealsResponse");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAgainstDefinition(JsonNode json, String definitionName) {
|
||||||
|
JsonNode def = schemaRoot.path("definitions").path(definitionName);
|
||||||
|
assertThat(def.isMissingNode()).as("Definição %s deve existir no schema", definitionName).isFalse();
|
||||||
|
|
||||||
|
JsonNode required = def.path("required");
|
||||||
|
if (required.isArray()) {
|
||||||
|
for (JsonNode reqNode : required) {
|
||||||
|
String field = reqNode.asText();
|
||||||
|
assertThat(json.hasNonNull(field))
|
||||||
|
.as("Campo obrigatório '%s' da definição '%s' deve estar presente e não nulo", field, definitionName)
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode properties = def.path("properties");
|
||||||
|
if (properties.isObject()) {
|
||||||
|
properties.fieldNames().forEachRemaining(propName -> {
|
||||||
|
if (json.has(propName) && !json.get(propName).isNull()) {
|
||||||
|
JsonNode propDef = properties.get(propName);
|
||||||
|
String expectedType = propDef.path("type").asText(null);
|
||||||
|
if (expectedType != null) {
|
||||||
|
assertTypeMatches(json.get(propName), expectedType, propName, definitionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTypeMatches(JsonNode valueNode, String expectedType, String propName, String defName) {
|
||||||
|
switch (expectedType) {
|
||||||
|
case "string" -> assertThat(valueNode.isTextual())
|
||||||
|
.as("Propriedade '%s' em '%s' deve ser string", propName, defName).isTrue();
|
||||||
|
case "boolean" -> assertThat(valueNode.isBoolean())
|
||||||
|
.as("Propriedade '%s' em '%s' deve ser boolean", propName, defName).isTrue();
|
||||||
|
case "number" -> assertThat(valueNode.isNumber())
|
||||||
|
.as("Propriedade '%s' em '%s' deve ser number", propName, defName).isTrue();
|
||||||
|
case "integer" -> assertThat(valueNode.isIntegralNumber())
|
||||||
|
.as("Propriedade '%s' em '%s' deve ser integer", propName, defName).isTrue();
|
||||||
|
case "array" -> assertThat(valueNode.isArray())
|
||||||
|
.as("Propriedade '%s' em '%s' deve ser array", propName, defName).isTrue();
|
||||||
|
case "object" -> assertThat(valueNode.isObject())
|
||||||
|
.as("Propriedade '%s' em '%s' deve ser object", propName, defName).isTrue();
|
||||||
|
default -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,6 +198,63 @@ class CcspServerTest {
|
|||||||
assertThat(resp.body()).contains("Teste ICP-Brasil");
|
assertThat(resp.body()).contains("Teste ICP-Brasil");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /certificates deve rejeitar com 401 requisições sem application_id")
|
||||||
|
void testGetCertificatesShouldRejectWhenAppIdMissing() throws Exception {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/certificates"))
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(401);
|
||||||
|
assertThat(resp.body()).contains("APPLICATION_NOT_AUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Requisição OPTIONS com origem desconhecida deve ser rejeitada com 403")
|
||||||
|
void testCorsPreflightShouldRejectUnauthorizedOrigin() throws Exception {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||||
|
.method("OPTIONS", HttpRequest.BodyPublishers.noBody())
|
||||||
|
.header("Origin", "https://malicious-attacker.com")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<Void> resp = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /sign deve rejeitar requisição sem nonce")
|
||||||
|
void testSignShouldRejectWhenNonceIsMissing() throws Exception {
|
||||||
|
byte[] docHash = MessageDigest.getInstance("SHA-256").digest("Documento".getBytes());
|
||||||
|
String hashBase64 = Base64.getEncoder().encodeToString(docHash);
|
||||||
|
|
||||||
|
String jsonPayload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-test",
|
||||||
|
"operation": "sign",
|
||||||
|
"algorithm": "SHA256withRSA",
|
||||||
|
"digest": "%s",
|
||||||
|
"certificate_hint": "cert-1"
|
||||||
|
}
|
||||||
|
""".formatted(UUID.randomUUID(), hashBase64);
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(400);
|
||||||
|
assertThat(resp.body()).contains("REPLAY_DETECTED");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Requisição OPTIONS deve retornar headers CORS válidos")
|
@DisplayName("Requisição OPTIONS deve retornar headers CORS válidos")
|
||||||
void testCorsPreflight() throws Exception {
|
void testCorsPreflight() throws Exception {
|
||||||
@@ -398,7 +455,7 @@ class CcspServerTest {
|
|||||||
assertThat(resp.body()).contains("\"status\":\"success\"");
|
assertThat(resp.body()).contains("\"status\":\"success\"");
|
||||||
assertThat(resp.body()).contains("\"seals\":");
|
assertThat(resp.body()).contains("\"seals\":");
|
||||||
assertThat(resp.body()).contains("icp-brasil");
|
assertThat(resp.body()).contains("icp-brasil");
|
||||||
assertThat(resp.body()).contains("gov-br");
|
assertThat(resp.body()).contains("ICP-Brasil Institucional");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -558,4 +615,42 @@ class CcspServerTest {
|
|||||||
assertThat(resp.body()).contains("\"signed_xml\":");
|
assertThat(resp.body()).contains("\"signed_xml\":");
|
||||||
assertThat(resp.body()).contains("QualifyingProperties");
|
assertThat(resp.body()).contains("QualifyingProperties");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Requisição com cabeçalho Host não-loopback (DNS Rebinding) deve ser rejeitada com 403")
|
||||||
|
void testDnsRebindingHostRejection() throws Exception {
|
||||||
|
int port = server.getPort();
|
||||||
|
try (java.net.Socket socket = new java.net.Socket("127.0.0.1", port);
|
||||||
|
java.io.OutputStream out = socket.getOutputStream();
|
||||||
|
java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream()))) {
|
||||||
|
|
||||||
|
String rawRequest = "GET /ccsp/v1/status HTTP/1.1\r\n" +
|
||||||
|
"Host: evil.attacker.com\r\n" +
|
||||||
|
"Connection: close\r\n\r\n";
|
||||||
|
out.write(rawRequest.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String statusLine = in.readLine();
|
||||||
|
assertThat(statusLine).contains("403");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Requisição com cabeçalho Host loopback válido deve ser aceita")
|
||||||
|
void testValidLoopbackHostAcceptance() throws Exception {
|
||||||
|
int port = server.getPort();
|
||||||
|
try (java.net.Socket socket = new java.net.Socket("127.0.0.1", port);
|
||||||
|
java.io.OutputStream out = socket.getOutputStream();
|
||||||
|
java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream()))) {
|
||||||
|
|
||||||
|
String rawRequest = "GET /ccsp/v1/status HTTP/1.1\r\n" +
|
||||||
|
"Host: 127.0.0.1:" + port + "\r\n" +
|
||||||
|
"Connection: close\r\n\r\n";
|
||||||
|
out.write(rawRequest.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String statusLine = in.readLine();
|
||||||
|
assertThat(statusLine).contains("200");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,4 +63,81 @@ class CertificateManagerTest {
|
|||||||
assertThat(byCnpj).isPresent();
|
assertThat(byCnpj).isPresent();
|
||||||
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
|
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve desativar provider PKCS#11 quando chave de diagnóstico estiver ativa")
|
||||||
|
void shouldRespectDiagnosticsNoPkcs11() {
|
||||||
|
System.setProperty("cybercore.diag.no-pkcs11", "true");
|
||||||
|
try {
|
||||||
|
CertificateManager manager = CertificateManager.createDefault();
|
||||||
|
assertThat(manager.getProviders())
|
||||||
|
.extracting(CertificateProvider::getProviderId)
|
||||||
|
.doesNotContain("provider-pkcs11")
|
||||||
|
.contains("provider-a1");
|
||||||
|
} finally {
|
||||||
|
System.clearProperty("cybercore.diag.no-pkcs11");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve desativar provider Windows Certificate Store quando chave de diagnóstico estiver ativa")
|
||||||
|
void shouldRespectDiagnosticsNoWinCert() {
|
||||||
|
System.setProperty("cybercore.diag.no-wincert", "true");
|
||||||
|
try {
|
||||||
|
CertificateManager manager = CertificateManager.createDefault();
|
||||||
|
assertThat(manager.getProviders())
|
||||||
|
.extracting(CertificateProvider::getProviderId)
|
||||||
|
.doesNotContain("provider-win-store")
|
||||||
|
.contains("provider-a1");
|
||||||
|
} finally {
|
||||||
|
System.clearProperty("cybercore.diag.no-wincert");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve reutilizar cache sob demanda dentro do TTL e invalidar sob forceRefresh ou hardware events")
|
||||||
|
void shouldReuseCacheWithinTtlAndInvalidateOnHardwareEvents() throws Exception {
|
||||||
|
CertificateManager manager = new CertificateManager(5_000L); // 5 segundos TTL
|
||||||
|
|
||||||
|
CertificateProvider provider = mock(CertificateProvider.class);
|
||||||
|
when(provider.getProviderId()).thenReturn("mock-prov");
|
||||||
|
when(provider.isAvailable()).thenReturn(true);
|
||||||
|
|
||||||
|
ManagedCertificate cert = new ManagedCertificate(
|
||||||
|
"c1", "Mock Cert", CertificateType.A1, CertificateStatus.AVAILABLE,
|
||||||
|
"CN=Mock", "CN=AC", "1", "s1", "s2",
|
||||||
|
Instant.now().minus(1, ChronoUnit.DAYS), Instant.now().plus(30, ChronoUnit.DAYS),
|
||||||
|
"RSA", 2048, "Mock", null, null, "mock-prov", null
|
||||||
|
);
|
||||||
|
when(provider.listCertificates()).thenReturn(List.of(cert));
|
||||||
|
|
||||||
|
manager.registerProvider(provider);
|
||||||
|
|
||||||
|
// 1ª chamada: consulta o provider
|
||||||
|
List<ManagedCertificate> first = manager.discoverCertificates();
|
||||||
|
assertThat(first).hasSize(1);
|
||||||
|
org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(1)).listCertificates();
|
||||||
|
|
||||||
|
// 2ª chamada: dentro do TTL de 5s, não deve chamar o provider novamente
|
||||||
|
List<ManagedCertificate> second = manager.discoverCertificates();
|
||||||
|
assertThat(second).isSameAs(first);
|
||||||
|
org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(1)).listCertificates();
|
||||||
|
|
||||||
|
// 3ª chamada com forceRefresh=true: deve chamar o provider
|
||||||
|
List<ManagedCertificate> third = manager.discoverCertificates(true);
|
||||||
|
assertThat(third).hasSize(1);
|
||||||
|
org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(2)).listCertificates();
|
||||||
|
|
||||||
|
// 4ª chamada: simula evento de hardware (inserção de cartão)
|
||||||
|
manager.onCardInserted("Terminal-USB-1");
|
||||||
|
List<ManagedCertificate> fourth = manager.discoverCertificates();
|
||||||
|
assertThat(fourth).hasSize(1);
|
||||||
|
org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(3)).listCertificates();
|
||||||
|
|
||||||
|
// 5ª chamada: simula evento de hardware (remoção de cartão)
|
||||||
|
manager.onCardRemoved("Terminal-USB-1");
|
||||||
|
List<ManagedCertificate> fifth = manager.discoverCertificates();
|
||||||
|
assertThat(fifth).hasSize(1);
|
||||||
|
org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(4)).listCertificates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
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 org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.junit.jupiter.api.Assumptions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
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.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("Integração Real PKCS#11 / Hardware via SoftHSM2")
|
||||||
|
class SoftHsmPkcs11IntegrationTest {
|
||||||
|
|
||||||
|
private static final String[] CANDIDATE_SOFTHSM_LIBS = {
|
||||||
|
"/usr/lib/softhsm/libsofthsm2.so",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
|
||||||
|
"/usr/local/lib/softhsm/libsofthsm2.so",
|
||||||
|
"C:/SoftHSM2/lib/softhsm2.dll"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String foundLibrary;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void checkSoftHsmAvailability() {
|
||||||
|
for (String candidate : CANDIDATE_SOFTHSM_LIBS) {
|
||||||
|
File f = new File(candidate);
|
||||||
|
if (f.exists() && f.canRead()) {
|
||||||
|
foundLibrary = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve inicializar token PKCS#11, listar certificados e assinar digest via SunPKCS11")
|
||||||
|
void shouldSignDigestUsingRealPkcs11HardwareToken(@TempDir Path tempDir) throws Exception {
|
||||||
|
Assumptions.assumeTrue(foundLibrary != null,
|
||||||
|
"SoftHSM2 não encontrado no sistema operacional. Teste ignorado (será executado no CI Linux).");
|
||||||
|
|
||||||
|
// 1. Configura diretório isolado de tokens do SoftHSM2
|
||||||
|
Path tokensDir = tempDir.resolve("tokens");
|
||||||
|
File tokensFolder = tokensDir.toFile();
|
||||||
|
boolean created = tokensFolder.mkdirs();
|
||||||
|
assertThat(created || tokensFolder.exists()).isTrue();
|
||||||
|
|
||||||
|
Path confFile = tempDir.resolve("softhsm2.conf");
|
||||||
|
try (FileWriter writer = new FileWriter(confFile.toFile(), StandardCharsets.UTF_8)) {
|
||||||
|
writer.write("directories.tokendir = " + tokensFolder.getAbsolutePath() + "\n");
|
||||||
|
writer.write("objectstore.backend = file\n");
|
||||||
|
writer.write("log.level = ERROR\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Inicializa o token via softhsm2-util se disponível
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
"softhsm2-util",
|
||||||
|
"--init-token",
|
||||||
|
"--slot", "0",
|
||||||
|
"--label", "CyberCore-CI-Token",
|
||||||
|
"--so-pin", "87654321",
|
||||||
|
"--pin", "1234"
|
||||||
|
);
|
||||||
|
pb.environment().put("SOFTHSM2_CONF", confFile.toAbsolutePath().toString());
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
|
||||||
|
Process process = pb.start();
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
Assumptions.assumeTrue(exitCode == 0, "softhsm2-util falhou ou não está no PATH. Teste ignorado.");
|
||||||
|
|
||||||
|
// 3. Gera par de chaves RSA e certificado para importar no token via KeyStore PKCS11
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
KeyPair keyPair = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
X500Name subject = new X500Name("CN=CERTIFICADO TESTE PKCS11, O=ICP-Brasil Teste, C=BR");
|
||||||
|
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
|
||||||
|
Date notBefore = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
|
||||||
|
Date notAfter = Date.from(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||||
|
|
||||||
|
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
|
||||||
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
|
||||||
|
subject, serial, notBefore, notAfter, subject, subPubKeyInfo
|
||||||
|
);
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
|
||||||
|
X509CertificateHolder holder = certBuilder.build(signer);
|
||||||
|
X509Certificate testCert = new JcaX509CertificateConverter().getCertificate(holder);
|
||||||
|
|
||||||
|
// 4. Inicializa o Pkcs11CertificateProvider
|
||||||
|
Pkcs11CertificateProvider provider = new Pkcs11CertificateProvider();
|
||||||
|
provider.initializeWithPin(foundLibrary, 0, "1234".toCharArray());
|
||||||
|
|
||||||
|
// Importa a chave gerada para o token caso ainda não haja chaves
|
||||||
|
KeyStore ks = KeyStore.getInstance("PKCS11", java.security.Security.getProvider("SunPKCS11"));
|
||||||
|
try {
|
||||||
|
ks.setKeyEntry("teste-alias", keyPair.getPrivate(), "1234".toCharArray(), new Certificate[]{testCert});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Alguns drivers PKCS11 não suportam setKeyEntry diretamente sem C_CreateObject
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ManagedCertificate> certs = provider.listCertificates();
|
||||||
|
assertThat(provider.isAvailable()).isTrue();
|
||||||
|
|
||||||
|
// 5. Teste de assinatura de digest via SigningContext
|
||||||
|
byte[] payload = "CyberCore Systems Assinatura Digital PKCS#11".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload);
|
||||||
|
|
||||||
|
SigningContext context = new SigningContext(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
"app-test-id",
|
||||||
|
digest,
|
||||||
|
"SHA256withRSA",
|
||||||
|
certs.isEmpty() ? null : certs.get(0).id(),
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!certs.isEmpty()) {
|
||||||
|
SigningResult result = provider.sign(context);
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.signatureBytes()).isNotEmpty();
|
||||||
|
|
||||||
|
// Validação matemática da assinatura com a chave pública
|
||||||
|
Signature jcaSig = Signature.getInstance("SHA256withRSA");
|
||||||
|
jcaSig.initVerify(testCert.getPublicKey());
|
||||||
|
jcaSig.update(digest);
|
||||||
|
assertThat(jcaSig.verify(result.signatureBytes())).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,4 +44,29 @@ class ApplicationRegistryTest {
|
|||||||
reloaded.revoke("app-123");
|
reloaded.revoke("app-123");
|
||||||
assertThat(reloaded.findById("app-123")).isEmpty();
|
assertThat(reloaded.findById("app-123")).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve descartar aplicações e alertar se o arquivo json for adulterado sem HMAC correspondente")
|
||||||
|
void testTamperDetection(@TempDir Path tempDir) throws Exception {
|
||||||
|
Path jsonPath = tempDir.resolve("authorized-apps.json");
|
||||||
|
ApplicationRegistry registry = new ApplicationRegistry(jsonPath);
|
||||||
|
|
||||||
|
AuthorizedApplication app = new AuthorizedApplication(
|
||||||
|
"app-1",
|
||||||
|
"Legit App",
|
||||||
|
Set.of("https://legit.com"),
|
||||||
|
true,
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
registry.register(app);
|
||||||
|
|
||||||
|
// Adultera o arquivo JSON diretamente no disco simulando malware local
|
||||||
|
java.nio.file.Files.writeString(jsonPath, "[{\"applicationId\":\"malicious\",\"name\":\"Malware\",\"allowedOrigins\":[\"*\"],\"active\":true}]");
|
||||||
|
|
||||||
|
// Novo registry tenta carregar
|
||||||
|
ApplicationRegistry tamperedRegistry = new ApplicationRegistry(jsonPath);
|
||||||
|
assertThat(tamperedRegistry.findById("malicious")).isEmpty();
|
||||||
|
assertThat(tamperedRegistry.findById("app-1")).isEmpty();
|
||||||
|
assertThat(tamperedRegistry.listAll()).isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package br.com.cybercore.signer.core.security;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("Testes de Integridade Criptográfica (HmacIntegrityManager)")
|
||||||
|
class HmacIntegrityManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve gerar HMAC-SHA256 válido e verificar correspondência em tempo constante")
|
||||||
|
void testComputeAndVerifyHmac(@TempDir Path tempDir) {
|
||||||
|
Path keyPath = tempDir.resolve(".vault_key");
|
||||||
|
HmacIntegrityManager manager = new HmacIntegrityManager(keyPath);
|
||||||
|
|
||||||
|
byte[] payload = "{\"test\":\"cybercore\"}".getBytes(StandardCharsets.UTF_8);
|
||||||
|
String hmac = manager.computeHmacHex(payload);
|
||||||
|
|
||||||
|
assertThat(hmac).isNotEmpty().hasSize(64); // 256 bits = 64 hex chars
|
||||||
|
assertThat(manager.verifyHmac(payload, hmac)).isTrue();
|
||||||
|
assertThat(manager.verifyHmac("tampered".getBytes(StandardCharsets.UTF_8), hmac)).isFalse();
|
||||||
|
assertThat(manager.verifyHmac(payload, "invalidhmac")).isFalse();
|
||||||
|
assertThat(manager.verifyHmac(null, hmac)).isFalse();
|
||||||
|
assertThat(manager.verifyHmac(payload, null)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve reutilizar chave persistida em disco")
|
||||||
|
void testKeyPersistence(@TempDir Path tempDir) {
|
||||||
|
Path keyPath = tempDir.resolve(".vault_key");
|
||||||
|
HmacIntegrityManager manager1 = new HmacIntegrityManager(keyPath);
|
||||||
|
|
||||||
|
byte[] payload = "data-to-protect".getBytes(StandardCharsets.UTF_8);
|
||||||
|
String hmac1 = manager1.computeHmacHex(payload);
|
||||||
|
|
||||||
|
// Novo manager apontando para a mesma chave persistida
|
||||||
|
HmacIntegrityManager manager2 = new HmacIntegrityManager(keyPath);
|
||||||
|
String hmac2 = manager2.computeHmacHex(payload);
|
||||||
|
|
||||||
|
assertThat(hmac1).isEqualTo(hmac2);
|
||||||
|
assertThat(manager2.verifyHmac(payload, hmac1)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve restringir permissões de arquivo e diretório")
|
||||||
|
void testRestrictPermissions(@TempDir Path tempDir) throws Exception {
|
||||||
|
Path testFile = tempDir.resolve("protected.txt");
|
||||||
|
Files.writeString(testFile, "sensitive-data");
|
||||||
|
|
||||||
|
HmacIntegrityManager.restrictPermissions(testFile);
|
||||||
|
assertThat(Files.isReadable(testFile)).isTrue();
|
||||||
|
assertThat(Files.isWritable(testFile)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,4 +28,84 @@ class NonceManagerTest {
|
|||||||
assertThat(manager.registerNonce(null)).isFalse();
|
assertThat(manager.registerNonce(null)).isFalse();
|
||||||
assertThat(manager.registerNonce(" ")).isFalse();
|
assertThat(manager.registerNonce(" ")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve permitir apenas 1 registro concorrente simultâneo para o mesmo nonce")
|
||||||
|
void shouldBeThreadSafeUnderHighConcurrency() throws Exception {
|
||||||
|
NonceManager manager = new NonceManager();
|
||||||
|
String identicalNonce = "shared-race-nonce-" + java.util.UUID.randomUUID();
|
||||||
|
int threads = 30;
|
||||||
|
|
||||||
|
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threads);
|
||||||
|
java.util.concurrent.CountDownLatch startLatch = new java.util.concurrent.CountDownLatch(1);
|
||||||
|
java.util.concurrent.atomic.AtomicInteger successes = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
|
java.util.concurrent.atomic.AtomicInteger rejections = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
|
|
||||||
|
java.util.List<java.util.concurrent.Future<?>> futures = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < threads; i++) {
|
||||||
|
futures.add(executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
startLatch.await();
|
||||||
|
boolean registered = manager.registerNonce(identicalNonce);
|
||||||
|
if (registered) {
|
||||||
|
successes.incrementAndGet();
|
||||||
|
} else {
|
||||||
|
rejections.incrementAndGet();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
startLatch.countDown();
|
||||||
|
for (java.util.concurrent.Future<?> f : futures) {
|
||||||
|
f.get();
|
||||||
|
}
|
||||||
|
executor.shutdown();
|
||||||
|
|
||||||
|
assertThat(successes.get()).isEqualTo(1);
|
||||||
|
assertThat(rejections.get()).isEqualTo(threads - 1);
|
||||||
|
assertThat(manager.activeNonceCount()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve validar timestamp com tolerância a clock skew (passado recente e pequeno futuro)")
|
||||||
|
void shouldValidateTimestampWithClockSkew() {
|
||||||
|
NonceManager manager = new NonceManager(Duration.ofSeconds(60));
|
||||||
|
java.time.Instant now = java.time.Instant.now();
|
||||||
|
|
||||||
|
// Timestamp atual é válido
|
||||||
|
assertThat(manager.isTimestampValid(now)).isTrue();
|
||||||
|
|
||||||
|
// 10 segundos no passado (dentro da janela de 60s) é válido
|
||||||
|
assertThat(manager.isTimestampValid(now.minusSeconds(10))).isTrue();
|
||||||
|
|
||||||
|
// 20 segundos no futuro (dentro do clock skew de 30s) é tolerado
|
||||||
|
assertThat(manager.isTimestampValid(now.plusSeconds(20))).isTrue();
|
||||||
|
|
||||||
|
// 100 segundos no passado (fora de 60s + 30s = 90s) é rejeitado
|
||||||
|
assertThat(manager.isTimestampValid(now.minusSeconds(100))).isFalse();
|
||||||
|
|
||||||
|
// 60 segundos no futuro (além do skew de 30s) é rejeitado
|
||||||
|
assertThat(manager.isTimestampValid(now.plusSeconds(60))).isFalse();
|
||||||
|
|
||||||
|
// Nulo é rejeitado
|
||||||
|
assertThat(manager.isTimestampValid(null)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve limpar nonces expirados além da janela configurada")
|
||||||
|
void shouldCleanupExpiredNonces() throws Exception {
|
||||||
|
NonceManager manager = new NonceManager(Duration.ofMillis(50));
|
||||||
|
manager.registerNonce("temporary-nonce");
|
||||||
|
assertThat(manager.activeNonceCount()).isEqualTo(1);
|
||||||
|
|
||||||
|
Thread.sleep(70);
|
||||||
|
manager.cleanupExpired();
|
||||||
|
assertThat(manager.activeNonceCount()).isEqualTo(0);
|
||||||
|
|
||||||
|
// Após expirar e ser limpo, pode ser aceito novamente
|
||||||
|
assertThat(manager.registerNonce("temporary-nonce")).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,17 @@ class PinSessionManagerTest {
|
|||||||
assertThat(manager.hasPin("cert-a")).isFalse();
|
assertThat(manager.hasPin("cert-a")).isFalse();
|
||||||
assertThat(manager.hasPin("cert-b")).isFalse();
|
assertThat(manager.hasPin("cert-b")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve isolar PINs entre diferentes aplicações para o mesmo certificado")
|
||||||
|
void shouldIsolatePinByApplicationId() {
|
||||||
|
manager.savePin("app-1", "cert-x", "pin-app-1".toCharArray());
|
||||||
|
|
||||||
|
assertThat(manager.hasPin("app-1", "cert-x")).isTrue();
|
||||||
|
assertThat(manager.getPin("app-1", "cert-x")).isEqualTo("pin-app-1".toCharArray());
|
||||||
|
|
||||||
|
// Aplicação 2 não deve ter acesso ao PIN da Aplicação 1
|
||||||
|
assertThat(manager.hasPin("app-2", "cert-x")).isFalse();
|
||||||
|
assertThat(manager.getPin("app-2", "cert-x")).isNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package br.com.cybercore.signer.core.validation;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateProviderException;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||||
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
|
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DisplayName("Testes de Validação de Cadeia de Certificados X.509")
|
||||||
|
class CertificateChainValidatorTest {
|
||||||
|
|
||||||
|
private static KeyPair rootKeyPair;
|
||||||
|
private static KeyPair intermediateKeyPair;
|
||||||
|
private static KeyPair leafKeyPair;
|
||||||
|
|
||||||
|
private static X509Certificate rootCert;
|
||||||
|
private static X509Certificate intermediateCert;
|
||||||
|
private static X509Certificate leafCert;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void generateTestPki() throws Exception {
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
rootKeyPair = kpg.generateKeyPair();
|
||||||
|
intermediateKeyPair = kpg.generateKeyPair();
|
||||||
|
leafKeyPair = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
Date notBefore = Date.from(Instant.now().minus(10, ChronoUnit.DAYS));
|
||||||
|
Date notAfter = Date.from(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||||
|
|
||||||
|
// 1. AC Raiz (Autoassinada, CA=true)
|
||||||
|
X500Name rootName = new X500Name("CN=AC Raiz Teste, O=ICP-Brasil Teste, C=BR");
|
||||||
|
X509v3CertificateBuilder rootBuilder = new X509v3CertificateBuilder(
|
||||||
|
rootName, BigInteger.valueOf(1), notBefore, notAfter, rootName,
|
||||||
|
SubjectPublicKeyInfo.getInstance(rootKeyPair.getPublic().getEncoded())
|
||||||
|
);
|
||||||
|
rootBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
|
||||||
|
rootBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
|
||||||
|
ContentSigner rootSigner = new JcaContentSignerBuilder("SHA256withRSA").build(rootKeyPair.getPrivate());
|
||||||
|
rootCert = new JcaX509CertificateConverter().getCertificate(rootBuilder.build(rootSigner));
|
||||||
|
|
||||||
|
// 2. AC Intermediária (Assinada pela Raiz, CA=true)
|
||||||
|
X500Name intermediateName = new X500Name("CN=AC Intermediaria Teste, O=ICP-Brasil Teste, C=BR");
|
||||||
|
X509v3CertificateBuilder intermediateBuilder = new X509v3CertificateBuilder(
|
||||||
|
rootName, BigInteger.valueOf(2), notBefore, notAfter, intermediateName,
|
||||||
|
SubjectPublicKeyInfo.getInstance(intermediateKeyPair.getPublic().getEncoded())
|
||||||
|
);
|
||||||
|
intermediateBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
|
||||||
|
intermediateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
|
||||||
|
intermediateCert = new JcaX509CertificateConverter().getCertificate(intermediateBuilder.build(rootSigner));
|
||||||
|
|
||||||
|
// 3. Certificado Folha (Assinado pela Intermediária, CA=false)
|
||||||
|
X500Name leafName = new X500Name("CN=JOAO DA SILVA:12345678901, O=ICP-Brasil Teste, C=BR");
|
||||||
|
X509v3CertificateBuilder leafBuilder = new X509v3CertificateBuilder(
|
||||||
|
intermediateName, BigInteger.valueOf(3), notBefore, notAfter, leafName,
|
||||||
|
SubjectPublicKeyInfo.getInstance(leafKeyPair.getPublic().getEncoded())
|
||||||
|
);
|
||||||
|
leafBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
|
||||||
|
leafBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation));
|
||||||
|
ContentSigner intermediateSigner = new JcaContentSignerBuilder("SHA256withRSA").build(intermediateKeyPair.getPrivate());
|
||||||
|
leafCert = new JcaX509CertificateConverter().getCertificate(leafBuilder.build(intermediateSigner));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Cadeia Válida Completa: deve validar com sucesso contra raízes confiáveis")
|
||||||
|
void shouldValidateCompliantChainSuccessfully() {
|
||||||
|
List<X509Certificate> chain = List.of(leafCert, intermediateCert, rootCert);
|
||||||
|
Set<X509Certificate> trustedRoots = Set.of(rootCert);
|
||||||
|
|
||||||
|
assertThatCode(() -> CertificateChainValidator.validateChain(chain, trustedRoots))
|
||||||
|
.doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Certificado Expirado: deve lançar CERTIFICATE_EXPIRED")
|
||||||
|
void shouldThrowCertificateExpiredWhenReferenceDateIsBeyondValidity() {
|
||||||
|
List<X509Certificate> chain = List.of(leafCert, intermediateCert, rootCert);
|
||||||
|
Date futureDate = Date.from(Instant.now().plus(400, ChronoUnit.DAYS));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> CertificateChainValidator.validateChain(chain, Set.of(rootCert), futureDate))
|
||||||
|
.isInstanceOf(CertificateProviderException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
CertificateProviderException cpe = (CertificateProviderException) ex;
|
||||||
|
assertThat(cpe.getErrorCode()).isEqualTo(CcspErrorCode.CERTIFICATE_EXPIRED);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Assinatura Corrompida: deve lançar CERTIFICATE_INVALID se a assinatura de um elo falhar")
|
||||||
|
void shouldThrowCertificateInvalidWhenSignatureIsCorrupted() throws Exception {
|
||||||
|
// Gera um cert folha assinado por outra chave que não a intermediária
|
||||||
|
KeyPair fakeKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||||
|
ContentSigner fakeSigner = new JcaContentSignerBuilder("SHA256withRSA").build(fakeKeyPair.getPrivate());
|
||||||
|
|
||||||
|
X500Name intermediateName = new X500Name("CN=AC Intermediaria Teste, O=ICP-Brasil Teste, C=BR");
|
||||||
|
X500Name leafName = new X500Name("CN=ADULTERADO, C=BR");
|
||||||
|
X509v3CertificateBuilder fakeBuilder = new X509v3CertificateBuilder(
|
||||||
|
intermediateName, BigInteger.valueOf(99),
|
||||||
|
Date.from(Instant.now().minus(1, ChronoUnit.DAYS)),
|
||||||
|
Date.from(Instant.now().plus(30, ChronoUnit.DAYS)),
|
||||||
|
leafName,
|
||||||
|
SubjectPublicKeyInfo.getInstance(leafKeyPair.getPublic().getEncoded())
|
||||||
|
);
|
||||||
|
X509Certificate corruptedLeaf = new JcaX509CertificateConverter().getCertificate(fakeBuilder.build(fakeSigner));
|
||||||
|
|
||||||
|
List<X509Certificate> corruptedChain = List.of(corruptedLeaf, intermediateCert, rootCert);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> CertificateChainValidator.validateChain(corruptedChain, Set.of(rootCert)))
|
||||||
|
.isInstanceOf(CertificateProviderException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
CertificateProviderException cpe = (CertificateProviderException) ex;
|
||||||
|
assertThat(cpe.getErrorCode()).isEqualTo(CcspErrorCode.CERTIFICATE_INVALID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Cadeia Incompleta / Desconexa: deve lançar CERTIFICATE_INVALID")
|
||||||
|
void shouldThrowCertificateInvalidWhenChainIsBroken() {
|
||||||
|
// Passa folha diretamente apontando para raiz sem o intermediário
|
||||||
|
List<X509Certificate> brokenChain = List.of(leafCert, rootCert);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> CertificateChainValidator.validateChain(brokenChain, Set.of(rootCert)))
|
||||||
|
.isInstanceOf(CertificateProviderException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
CertificateProviderException cpe = (CertificateProviderException) ex;
|
||||||
|
assertThat(cpe.getErrorCode()).isEqualTo(CcspErrorCode.CERTIFICATE_INVALID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Raiz Não Confiável: deve lançar CERTIFICATE_INVALID quando a raiz não está no TrustStore")
|
||||||
|
void shouldThrowCertificateInvalidWhenRootIsNotTrusted() throws Exception {
|
||||||
|
KeyPair otherRootKey = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||||
|
X500Name otherRootName = new X500Name("CN=Outra Raiz Estranha, C=BR");
|
||||||
|
X509v3CertificateBuilder otherBuilder = new X509v3CertificateBuilder(
|
||||||
|
otherRootName, BigInteger.ONE,
|
||||||
|
Date.from(Instant.now().minus(1, ChronoUnit.DAYS)),
|
||||||
|
Date.from(Instant.now().plus(30, ChronoUnit.DAYS)),
|
||||||
|
otherRootName,
|
||||||
|
SubjectPublicKeyInfo.getInstance(otherRootKey.getPublic().getEncoded())
|
||||||
|
);
|
||||||
|
otherBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
|
||||||
|
ContentSigner otherSigner = new JcaContentSignerBuilder("SHA256withRSA").build(otherRootKey.getPrivate());
|
||||||
|
X509Certificate untrustedRoot = new JcaX509CertificateConverter().getCertificate(otherBuilder.build(otherSigner));
|
||||||
|
|
||||||
|
List<X509Certificate> chain = List.of(leafCert, intermediateCert, rootCert);
|
||||||
|
Set<X509Certificate> trustedRoots = Set.of(untrustedRoot); // Não inclui a rootCert da cadeia
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> CertificateChainValidator.validateChain(chain, trustedRoots))
|
||||||
|
.isInstanceOf(CertificateProviderException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
CertificateProviderException cpe = (CertificateProviderException) ex;
|
||||||
|
assertThat(cpe.getErrorCode()).isEqualTo(CcspErrorCode.CERTIFICATE_INVALID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Cadeia Vazia: deve lançar CERTIFICATE_NOT_FOUND")
|
||||||
|
void shouldThrowCertificateNotFoundForEmptyChain() {
|
||||||
|
assertThatThrownBy(() -> CertificateChainValidator.validateChain(List.of(), null))
|
||||||
|
.isInstanceOf(CertificateProviderException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
CertificateProviderException cpe = (CertificateProviderException) ex;
|
||||||
|
assertThat(cpe.getErrorCode()).isEqualTo(CcspErrorCode.CERTIFICATE_NOT_FOUND);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,4 +53,41 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>jacoco-check</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.05</minimum>
|
||||||
|
</limit>
|
||||||
|
<limit>
|
||||||
|
<counter>INSTRUCTION</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.05</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -72,15 +72,25 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CyberCoreDesktopApplication.class);
|
private static final Logger logger = LoggerFactory.getLogger(CyberCoreDesktopApplication.class);
|
||||||
|
|
||||||
private static boolean developmentMode = false;
|
private static volatile boolean developmentMode = false;
|
||||||
private static MainAppView mainAppView;
|
private static MainAppView mainAppView;
|
||||||
|
|
||||||
|
private static volatile CcspServer sharedCcspServer = null;
|
||||||
|
private static volatile CertificateManager sharedCertManager = null;
|
||||||
|
private static volatile SigningService sharedSigningService = null;
|
||||||
|
|
||||||
private CertificateManager certificateManager;
|
private CertificateManager certificateManager;
|
||||||
private SigningService signingService;
|
private SigningService signingService;
|
||||||
private CcspServer ccspServer;
|
private CcspServer ccspServer;
|
||||||
private SystemTrayManager systemTrayManager;
|
private SystemTrayManager systemTrayManager;
|
||||||
private SmartCardWatcher smartCardWatcher;
|
private SmartCardWatcher smartCardWatcher;
|
||||||
|
|
||||||
|
public static void setSharedServices(CcspServer server, CertificateManager certManager, SigningService signingService) {
|
||||||
|
sharedCcspServer = server;
|
||||||
|
sharedCertManager = certManager;
|
||||||
|
sharedSigningService = signingService;
|
||||||
|
}
|
||||||
|
|
||||||
public static void setDevelopmentMode(boolean devMode) {
|
public static void setDevelopmentMode(boolean devMode) {
|
||||||
developmentMode = devMode;
|
developmentMode = devMode;
|
||||||
}
|
}
|
||||||
@@ -93,10 +103,12 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
public void start(Stage primaryStage) {
|
public void start(Stage primaryStage) {
|
||||||
logger.info("Iniciando interface gráfica JavaFX...");
|
logger.info("Iniciando interface gráfica JavaFX...");
|
||||||
Platform.setImplicitExit(false);
|
Platform.setImplicitExit(false);
|
||||||
|
br.com.cybercore.signer.ui.prompt.JavaFxPromptListener.markToolkitInitialized();
|
||||||
|
br.com.cybercore.signer.ui.prompt.JavaFxPromptListener.setActiveStage(primaryStage);
|
||||||
|
|
||||||
// Inicializa serviços centrais reais da plataforma
|
// Inicializa ou reutiliza serviços centrais reais da plataforma
|
||||||
this.certificateManager = CertificateManager.createDefault();
|
this.certificateManager = (sharedCertManager != null) ? sharedCertManager : CertificateManager.createDefault();
|
||||||
this.signingService = new SigningService(this.certificateManager);
|
this.signingService = (sharedSigningService != null) ? sharedSigningService : new SigningService(this.certificateManager);
|
||||||
|
|
||||||
if (developmentMode) {
|
if (developmentMode) {
|
||||||
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
|
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
|
||||||
@@ -110,17 +122,18 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
|
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
|
||||||
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show());
|
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show());
|
||||||
mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show());
|
mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show());
|
||||||
mainAppView.setOnCoSign(() -> new FileSignDialog(primaryStage, "Co-Assinar Documento", "*.*", SvgIcons.coSignIcon(), certificateManager, signingService).show());
|
|
||||||
mainAppView.setOnTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
|
|
||||||
mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show());
|
mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show());
|
||||||
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage, false).show());
|
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage).show());
|
||||||
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
|
|
||||||
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
|
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
|
||||||
mainAppView.setOnExit(() -> {
|
mainAppView.setOnExit(() -> {
|
||||||
logger.info("Encerrando CyberCore Signer pela interface gráfica.");
|
logger.info("Ação 'Sair' acionada na interface gráfica.");
|
||||||
primaryStage.close();
|
if (systemTrayManager != null) {
|
||||||
Platform.exit();
|
systemTrayManager.handleWindowCloseRequest();
|
||||||
System.exit(0);
|
} else {
|
||||||
|
primaryStage.close();
|
||||||
|
Platform.exit();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ações do Painel Lateral
|
// Ações do Painel Lateral
|
||||||
@@ -130,21 +143,44 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
|
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
|
||||||
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
|
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
|
||||||
|
|
||||||
|
// Observabilidade do ciclo de vida da janela
|
||||||
|
primaryStage.showingProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Visibilidade da janela principal JavaFX alterada: visível={}", newVal));
|
||||||
|
primaryStage.iconifiedProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Estado de minimização da janela JavaFX alterado: minimizada={}", newVal));
|
||||||
|
primaryStage.focusedProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Foco da janela principal alterado: focada={}", newVal));
|
||||||
|
primaryStage.xProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal movida: x={}, y={}", newVal, primaryStage.getY()));
|
||||||
|
primaryStage.yProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal movida: x={}, y={}", primaryStage.getX(), newVal));
|
||||||
|
primaryStage.widthProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal redimensionada: largura={}, altura={}", newVal, primaryStage.getHeight()));
|
||||||
|
primaryStage.heightProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal redimensionada: largura={}, altura={}", primaryStage.getWidth(), newVal));
|
||||||
|
|
||||||
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
|
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
|
||||||
initCcspServer(primaryStage);
|
initCcspServer(primaryStage);
|
||||||
|
|
||||||
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
|
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
|
||||||
initSmartCardWatcher();
|
initSmartCardWatcher();
|
||||||
|
|
||||||
// Configura bandeja do sistema (System Tray) para operação contínua em segundo plano
|
|
||||||
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
|
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
|
||||||
this.systemTrayManager.setup();
|
|
||||||
|
|
||||||
// Carrega os certificados instalados na máquina do usuário
|
// Carrega os certificados instalados na máquina do usuário de forma estável
|
||||||
refreshCertificates();
|
refreshCertificates();
|
||||||
|
|
||||||
|
// Exibe a janela principal em primeiro plano após os serviços estarem prontos
|
||||||
mainAppView.show();
|
mainAppView.show();
|
||||||
logger.info("Janela principal JavaFX exibida.");
|
logger.info("Janela principal JavaFX exibida.");
|
||||||
|
|
||||||
|
// Configura a bandeja (System Tray) somente depois que a janela JavaFX está visível,
|
||||||
|
// para não inicializar o toolkit AWT concorrentemente com a criação da janela.
|
||||||
|
if (Boolean.getBoolean("cybercore.no.tray")) {
|
||||||
|
logger.warn("System Tray desativado via -Dcybercore.no.tray=true (diagnóstico).");
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> this.systemTrayManager.setup());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshCertificates() {
|
private void refreshCertificates() {
|
||||||
@@ -395,6 +431,12 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initCcspServer(Stage stage) {
|
private void initCcspServer(Stage stage) {
|
||||||
|
if (sharedCcspServer != null) {
|
||||||
|
this.ccspServer = sharedCcspServer;
|
||||||
|
logger.info("Vinculada interface desktop ao servidor CCSP/1 já ativo na porta {}", this.ccspServer.getPort());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CcspUserPromptListener promptListener = new CcspUserPromptListener() {
|
CcspUserPromptListener promptListener = new CcspUserPromptListener() {
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
|
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
|
||||||
@@ -448,9 +490,31 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
.map(AuthorizedApplication::name)
|
.map(AuthorizedApplication::name)
|
||||||
.orElse(request.applicationId());
|
.orElse(request.applicationId());
|
||||||
|
|
||||||
|
Long docSize = request.documentSize();
|
||||||
|
if (docSize == null && request.pdf() != null) {
|
||||||
|
try {
|
||||||
|
docSize = (long) Base64.getDecoder().decode(request.pdf()).length;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.debug("Não foi possível decodificar base64 do PDF: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
} else if (docSize == null && request.xmlBase64() != null) {
|
||||||
|
try {
|
||||||
|
docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.debug("Não foi possível decodificar base64 do XML: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String requestOrigin = request.origin();
|
||||||
|
if (requestOrigin == null || requestOrigin.isBlank()) {
|
||||||
|
requestOrigin = ApplicationRegistry.getInstance().findById(request.applicationId())
|
||||||
|
.map(a -> String.join(", ", a.allowedOrigins()))
|
||||||
|
.orElse("Requisição Web Local");
|
||||||
|
}
|
||||||
|
|
||||||
SignaturePromptDialog dialog = new SignaturePromptDialog(
|
SignaturePromptDialog dialog = new SignaturePromptDialog(
|
||||||
stage, appName, "Requisição Web CCSP", certificate, context,
|
stage, appName, requestOrigin, certificate, context,
|
||||||
request.documentName(), request.documentType(), request.isAuthentication()
|
request.documentName(), request.documentType(), docSize, request.isAuthentication()
|
||||||
);
|
);
|
||||||
dialog.showAndAwait().whenComplete((approved, err) -> {
|
dialog.showAndAwait().whenComplete((approved, err) -> {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
@@ -468,16 +532,40 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
int targetPort = resolveConfiguredPort();
|
||||||
try {
|
try {
|
||||||
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
|
this.ccspServer = new CcspServer(targetPort, this.certificateManager, this.signingService, ApplicationRegistry.getInstance(), promptListener);
|
||||||
this.ccspServer.start();
|
this.ccspServer.start();
|
||||||
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
||||||
|
} catch (java.net.BindException be) {
|
||||||
|
logger.warn("ALERTA DE COLISÃO DE PORTA: A porta {} já está ocupada por outra instância do CyberCore Signer ou outro serviço. Para usar outra porta, configure CYBERCORE_PORT ou use --port <porta>. O agente continuará ativo na interface gráfica.", targetPort);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
|
||||||
|
logger.warn("ALERTA DE COLISÃO DE PORTA: A porta {} já está ocupada por outra instância do CyberCore Signer ou outro serviço. Para usar outra porta, configure CYBERCORE_PORT ou use --port <porta>. O agente continuará ativo na interface gráfica.", targetPort);
|
||||||
|
} else {
|
||||||
|
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int resolveConfiguredPort() {
|
||||||
|
String env = System.getenv("CYBERCORE_PORT");
|
||||||
|
if (env != null && !env.isBlank()) {
|
||||||
|
try { return Integer.parseInt(env.trim()); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
String prop = System.getProperty("cybercore.port");
|
||||||
|
if (prop != null && !prop.isBlank()) {
|
||||||
|
try { return Integer.parseInt(prop.trim()); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
return CcspServer.DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
private void initSmartCardWatcher() {
|
private void initSmartCardWatcher() {
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) {
|
||||||
|
logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.smartCardWatcher = new SmartCardWatcher();
|
this.smartCardWatcher = new SmartCardWatcher();
|
||||||
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
|
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
|
||||||
|
|||||||
@@ -519,16 +519,38 @@ public final class AssetManager {
|
|||||||
double x = parseDouble(el.getAttribute("x"), 0);
|
double x = parseDouble(el.getAttribute("x"), 0);
|
||||||
double y = parseDouble(el.getAttribute("y"), 0);
|
double y = parseDouble(el.getAttribute("y"), 0);
|
||||||
String content = el.getTextContent();
|
String content = el.getTextContent();
|
||||||
Text t = new Text(x, y, content != null ? content.trim() : "");
|
Text t = new Text(content != null ? content.trim() : "");
|
||||||
double fs = parseDouble(el.getAttribute("font-size"), 12);
|
double fs = parseDouble(el.getAttribute("font-size"), 12);
|
||||||
String fw = el.getAttribute("font-weight");
|
String fw = el.getAttribute("font-weight");
|
||||||
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
|
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
|
||||||
if (el.hasAttribute("fill")) {
|
|
||||||
t.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.BLACK));
|
Map<String, String> inlineStyle = el.hasAttribute("style") ? parseStyleAttribute(el.getAttribute("style")) : Map.of();
|
||||||
|
String fillVal = inlineStyle.getOrDefault("fill", el.hasAttribute("fill") ? el.getAttribute("fill") : null);
|
||||||
|
if (fillVal != null) {
|
||||||
|
t.setFill(parsePaint(fillVal, defsMap, Color.BLACK));
|
||||||
}
|
}
|
||||||
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
|
|
||||||
|
String anchor = inlineStyle.getOrDefault("text-anchor", el.getAttribute("text-anchor"));
|
||||||
|
String baseline = inlineStyle.getOrDefault("dominant-baseline", el.getAttribute("dominant-baseline"));
|
||||||
|
|
||||||
|
boolean isCenterBaseline = "central".equalsIgnoreCase(baseline) || "middle".equalsIgnoreCase(baseline);
|
||||||
|
if (isCenterBaseline) {
|
||||||
t.setTextOrigin(javafx.geometry.VPos.CENTER);
|
t.setTextOrigin(javafx.geometry.VPos.CENTER);
|
||||||
|
} else {
|
||||||
|
t.setTextOrigin(javafx.geometry.VPos.BASELINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double textWidth = t.getLayoutBounds().getWidth();
|
||||||
|
double finalX = x;
|
||||||
|
if ("middle".equalsIgnoreCase(anchor)) {
|
||||||
|
finalX = x - (textWidth / 2.0);
|
||||||
|
} else if ("end".equalsIgnoreCase(anchor)) {
|
||||||
|
finalX = x - textWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.setX(finalX);
|
||||||
|
t.setY(y);
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
case "g" -> {
|
case "g" -> {
|
||||||
@@ -558,19 +580,46 @@ public final class AssetManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseStyleAttribute(String styleAttr) {
|
||||||
|
Map<String, String> styleMap = new HashMap<>();
|
||||||
|
if (styleAttr == null || styleAttr.isBlank()) {
|
||||||
|
return styleMap;
|
||||||
|
}
|
||||||
|
String[] declarations = styleAttr.split(";");
|
||||||
|
for (String decl : declarations) {
|
||||||
|
int colonIdx = decl.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
String prop = decl.substring(0, colonIdx).trim().toLowerCase();
|
||||||
|
String val = decl.substring(colonIdx + 1).trim();
|
||||||
|
styleMap.put(prop, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styleMap;
|
||||||
|
}
|
||||||
|
|
||||||
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
|
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
|
||||||
if (el.hasAttribute("fill")) {
|
Map<String, String> inlineStyle = el.hasAttribute("style") ? parseStyleAttribute(el.getAttribute("style")) : Map.of();
|
||||||
shape.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.TRANSPARENT));
|
|
||||||
|
String fillVal = inlineStyle.getOrDefault("fill", el.hasAttribute("fill") ? el.getAttribute("fill") : null);
|
||||||
|
if (fillVal != null) {
|
||||||
|
shape.setFill(parsePaint(fillVal, defsMap, Color.TRANSPARENT));
|
||||||
} else if (shape instanceof Line) {
|
} else if (shape instanceof Line) {
|
||||||
shape.setFill(Color.TRANSPARENT);
|
shape.setFill(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("stroke")) {
|
String strokeVal = inlineStyle.getOrDefault("stroke", el.hasAttribute("stroke") ? el.getAttribute("stroke") : null);
|
||||||
shape.setStroke(parsePaint(el.getAttribute("stroke"), defsMap, Color.TRANSPARENT));
|
if (strokeVal != null) {
|
||||||
|
shape.setStroke(parsePaint(strokeVal, defsMap, Color.TRANSPARENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("stroke-width")) {
|
String strokeWidthVal = inlineStyle.getOrDefault("stroke-width", el.hasAttribute("stroke-width") ? el.getAttribute("stroke-width") : null);
|
||||||
shape.setStrokeWidth(parseDouble(el.getAttribute("stroke-width"), 1.0));
|
if (strokeWidthVal != null) {
|
||||||
|
shape.setStrokeWidth(parseDouble(strokeWidthVal, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
String opacityVal = inlineStyle.getOrDefault("opacity", el.hasAttribute("opacity") ? el.getAttribute("opacity") : null);
|
||||||
|
if (opacityVal != null) {
|
||||||
|
shape.setOpacity(parseDouble(opacityVal, 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) {
|
if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package br.com.cybercore.signer.ui.prompt;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
|
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||||
|
import br.com.cybercore.signer.core.protocol.CcspRequest;
|
||||||
|
import br.com.cybercore.signer.core.protocol.CcspUserPromptListener;
|
||||||
|
import br.com.cybercore.signer.core.security.ApplicationRegistry;
|
||||||
|
import br.com.cybercore.signer.core.security.AuthorizedApplication;
|
||||||
|
import br.com.cybercore.signer.ui.view.ApplicationApprovalDialog;
|
||||||
|
import br.com.cybercore.signer.ui.view.SignaturePromptDialog;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener desacoplado de prompts do CCSP para a interface JavaFX.
|
||||||
|
* Garante que o toolkit JavaFX só seja inicializado sob demanda (on-demand)
|
||||||
|
* quando houver um diálogo visual de confirmação a ser exibido ao usuário.
|
||||||
|
*/
|
||||||
|
public class JavaFxPromptListener implements CcspUserPromptListener {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JavaFxPromptListener.class);
|
||||||
|
private static final AtomicBoolean FX_TOOLKIT_INITIALIZED = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private static volatile Stage activeStage = null;
|
||||||
|
|
||||||
|
public static void setActiveStage(Stage stage) {
|
||||||
|
activeStage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void markToolkitInitialized() {
|
||||||
|
FX_TOOLKIT_INITIALIZED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runOnFxThread(Runnable action) {
|
||||||
|
if (FX_TOOLKIT_INITIALIZED.get()) {
|
||||||
|
Platform.runLater(action);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Platform.startup(() -> {
|
||||||
|
FX_TOOLKIT_INITIALIZED.set(true);
|
||||||
|
Platform.setImplicitExit(false);
|
||||||
|
action.run();
|
||||||
|
});
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
FX_TOOLKIT_INITIALIZED.set(true);
|
||||||
|
Platform.runLater(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
runOnFxThread(() -> {
|
||||||
|
try {
|
||||||
|
ApplicationApprovalDialog dialog = new ApplicationApprovalDialog(activeStage, origin, appName, applicationId);
|
||||||
|
dialog.showAndAwait().whenComplete((approved, err) -> {
|
||||||
|
if (err != null) {
|
||||||
|
future.complete(false);
|
||||||
|
} else {
|
||||||
|
future.complete(Boolean.TRUE.equals(approved));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Erro ao exibir diálogo de autorização de aplicação: {}", ex.getMessage());
|
||||||
|
future.complete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) {
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
runOnFxThread(() -> {
|
||||||
|
try {
|
||||||
|
byte[] digestBytes;
|
||||||
|
if (request.digest() != null && !request.digest().isBlank()) {
|
||||||
|
digestBytes = Base64.getDecoder().decode(request.digest());
|
||||||
|
} else if (request.pdf() != null && !request.pdf().isBlank()) {
|
||||||
|
byte[] rawPdf = Base64.getDecoder().decode(request.pdf());
|
||||||
|
digestBytes = MessageDigest.getInstance("SHA-256").digest(rawPdf);
|
||||||
|
} else if (request.challenge() != null && !request.challenge().isBlank()) {
|
||||||
|
digestBytes = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(request.challenge().getBytes(StandardCharsets.UTF_8));
|
||||||
|
} else {
|
||||||
|
digestBytes = new byte[32];
|
||||||
|
}
|
||||||
|
|
||||||
|
SigningContext context = new SigningContext(
|
||||||
|
request.requestId(),
|
||||||
|
request.applicationId(),
|
||||||
|
digestBytes,
|
||||||
|
request.algorithm() != null ? request.algorithm() : "SHA256withRSA",
|
||||||
|
certificate.id(),
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
String appName = ApplicationRegistry.getInstance().findById(request.applicationId())
|
||||||
|
.map(AuthorizedApplication::name)
|
||||||
|
.orElse(request.applicationId());
|
||||||
|
|
||||||
|
Long docSize = request.documentSize();
|
||||||
|
if (docSize == null && request.pdf() != null) {
|
||||||
|
try {
|
||||||
|
docSize = (long) Base64.getDecoder().decode(request.pdf()).length;
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
} else if (docSize == null && request.xmlBase64() != null) {
|
||||||
|
try {
|
||||||
|
docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length;
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String requestOrigin = request.origin();
|
||||||
|
if (requestOrigin == null || requestOrigin.isBlank()) {
|
||||||
|
requestOrigin = ApplicationRegistry.getInstance().findById(request.applicationId())
|
||||||
|
.map(a -> String.join(", ", a.allowedOrigins()))
|
||||||
|
.orElse("Requisição Web Local");
|
||||||
|
}
|
||||||
|
|
||||||
|
SignaturePromptDialog dialog = new SignaturePromptDialog(
|
||||||
|
activeStage, appName, requestOrigin, certificate, context,
|
||||||
|
request.documentName(), request.documentType(), docSize, request.isAuthentication()
|
||||||
|
);
|
||||||
|
dialog.showAndAwait().whenComplete((approved, err) -> {
|
||||||
|
if (err != null) {
|
||||||
|
future.complete(false);
|
||||||
|
} else {
|
||||||
|
future.complete(Boolean.TRUE.equals(approved));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Erro ao exibir diálogo de confirmação de assinatura: {}", ex.getMessage());
|
||||||
|
future.complete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package br.com.cybercore.signer.ui.tray;
|
package br.com.cybercore.signer.ui.tray;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.view.ExitConfirmationDialog;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -40,12 +41,19 @@ public class SystemTrayManager {
|
|||||||
/**
|
/**
|
||||||
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
|
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
|
||||||
*/
|
*/
|
||||||
public synchronized boolean setup() {
|
public boolean setup() {
|
||||||
if (!SystemTray.isSupported()) {
|
if (!SystemTray.isSupported()) {
|
||||||
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
|
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// O TrayIcon AWT é criado na thread de eventos do AWT, nunca na thread do JavaFX,
|
||||||
|
// para evitar conflito entre os dois loops de mensagens nativos no Windows.
|
||||||
|
java.awt.EventQueue.invokeLater(this::setupOnAwtThread);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setupOnAwtThread() {
|
||||||
try {
|
try {
|
||||||
SystemTray tray = SystemTray.getSystemTray();
|
SystemTray tray = SystemTray.getSystemTray();
|
||||||
Image iconImage = createTrayIconImage();
|
Image iconImage = createTrayIconImage();
|
||||||
@@ -76,38 +84,78 @@ public class SystemTrayManager {
|
|||||||
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
|
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
|
||||||
this.trayIcon.setImageAutoSize(true);
|
this.trayIcon.setImageAutoSize(true);
|
||||||
|
|
||||||
// Clique no ícone restaura a janela
|
// Duplo clique (ActionListener) e clique simples (MouseListener) restauram a janela
|
||||||
this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
|
this.trayIcon.addActionListener(e -> {
|
||||||
|
logger.info("Ícone da bandeja acionado (duplo clique).");
|
||||||
|
Platform.runLater(this::showStage);
|
||||||
|
});
|
||||||
|
this.trayIcon.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(java.awt.event.MouseEvent e) {
|
||||||
|
if (e.getButton() == java.awt.event.MouseEvent.BUTTON1) {
|
||||||
|
logger.info("Ícone da bandeja acionado (clique simples).");
|
||||||
|
Platform.runLater(SystemTrayManager.this::showStage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tray.add(this.trayIcon);
|
tray.add(this.trayIcon);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
// Intercepta fechamento da janela para ocultar em vez de encerrar
|
// Intercepta fechamento da janela (botão X) para confirmar ação com o usuário
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
stage.setOnCloseRequest(event -> {
|
stage.setOnCloseRequest(event -> {
|
||||||
event.consume();
|
event.consume();
|
||||||
stage.hide();
|
handleWindowCloseRequest();
|
||||||
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("System Tray configurado com sucesso.");
|
logger.info("System Tray configurado com sucesso.");
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (AWTException e) {
|
} catch (AWTException e) {
|
||||||
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
|
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
|
||||||
return false;
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage());
|
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage(), t);
|
||||||
return false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exibe o diálogo de confirmação ao clicar no 'X' da janela principal,
|
||||||
|
* permitindo minimizar para a bandeja, encerrar definitivamente ou cancelar.
|
||||||
|
*/
|
||||||
|
public void handleWindowCloseRequest() {
|
||||||
|
try {
|
||||||
|
ExitConfirmationDialog dialog = new ExitConfirmationDialog(stage);
|
||||||
|
ExitConfirmationDialog.ExitChoice choice = dialog.showAndGetChoice();
|
||||||
|
|
||||||
|
switch (choice) {
|
||||||
|
case MINIMIZE_TO_TRAY -> {
|
||||||
|
logger.info("Usuário optou por minimizar o CyberCore Signer para a bandeja.");
|
||||||
|
stage.hide();
|
||||||
|
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
|
||||||
|
}
|
||||||
|
case EXIT_COMPLETELY -> {
|
||||||
|
logger.info("Usuário optou por encerrar definitivamente o CyberCore Signer.");
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
case CANCEL -> {
|
||||||
|
logger.debug("Fechamento da janela cancelado pelo usuário.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Erro ao exibir diálogo de fechamento, minimizando para bandeja por segurança: {}", t.getMessage());
|
||||||
|
stage.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showStage() {
|
public void showStage() {
|
||||||
if (stage != null) {
|
if (stage != null) {
|
||||||
|
logger.info("Restaurando janela principal (visível={}, minimizada={}).",
|
||||||
|
stage.isShowing(), stage.isIconified());
|
||||||
stage.show();
|
stage.show();
|
||||||
stage.setIconified(false);
|
stage.setIconified(false);
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
|
stage.requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package br.com.cybercore.signer.ui.view;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
|
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diálogo modal de confirmação ao fechar a janela principal do CyberCore Signer.
|
||||||
|
* Permite ao usuário escolher entre manter o agente ativo na bandeja do sistema
|
||||||
|
* ou encerrar completamente a aplicação.
|
||||||
|
*/
|
||||||
|
public final class ExitConfirmationDialog {
|
||||||
|
|
||||||
|
public enum ExitChoice {
|
||||||
|
MINIMIZE_TO_TRAY,
|
||||||
|
EXIT_COMPLETELY,
|
||||||
|
CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Stage stage;
|
||||||
|
private ExitChoice chosenAction = ExitChoice.CANCEL;
|
||||||
|
|
||||||
|
public ExitConfirmationDialog(Stage owner) {
|
||||||
|
this.stage = new Stage();
|
||||||
|
this.stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
if (owner != null) {
|
||||||
|
this.stage.initOwner(owner);
|
||||||
|
}
|
||||||
|
this.stage.setTitle("CyberCore Signer — Encerrar ou Manter na Bandeja");
|
||||||
|
this.stage.setResizable(false);
|
||||||
|
this.stage.setMinWidth(520);
|
||||||
|
this.stage.setMinHeight(190);
|
||||||
|
|
||||||
|
initLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initLayout() {
|
||||||
|
VBox root = new VBox(22);
|
||||||
|
root.setPadding(new Insets(24));
|
||||||
|
root.getStyleClass().add("dialog-container");
|
||||||
|
|
||||||
|
// Cabeçalho com ícone e texto explicativo
|
||||||
|
HBox header = new HBox(16);
|
||||||
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
VBox titleBox = new VBox(4);
|
||||||
|
Label title = new Label("Como você deseja proceder?");
|
||||||
|
title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label subtitle = new Label(
|
||||||
|
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
|
||||||
|
"para atender assinaturas de aplicações web, ou encerrá-lo por completo."
|
||||||
|
);
|
||||||
|
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
||||||
|
|
||||||
|
titleBox.getChildren().addAll(title, subtitle);
|
||||||
|
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
|
||||||
|
|
||||||
|
// Barra de Ações com os botões
|
||||||
|
HBox actionsRow = new HBox(10);
|
||||||
|
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
Button btnCancel = new Button("Cancelar");
|
||||||
|
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog");
|
||||||
|
btnCancel.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.CANCEL;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox spacer = new HBox();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button btnExit = new Button("Encerrar Definitivamente");
|
||||||
|
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog");
|
||||||
|
btnExit.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.EXIT_COMPLETELY;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button btnMinimize = new Button("Minimizar para a Bandeja");
|
||||||
|
btnMinimize.getStyleClass().addAll("btn-primary", "btn-dialog");
|
||||||
|
btnMinimize.setDefaultButton(true);
|
||||||
|
btnMinimize.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.MINIMIZE_TO_TRAY;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
actionsRow.getChildren().addAll(btnCancel, spacer, btnExit, btnMinimize);
|
||||||
|
|
||||||
|
root.getChildren().addAll(header, actionsRow);
|
||||||
|
|
||||||
|
Scene scene = new Scene(root);
|
||||||
|
ThemeManager.applyTheme(scene);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExitChoice showAndGetChoice() {
|
||||||
|
stage.showAndWait();
|
||||||
|
return chosenAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import javafx.scene.layout.Region;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -27,21 +29,19 @@ import java.util.Objects;
|
|||||||
@SuppressWarnings("this-escape")
|
@SuppressWarnings("this-escape")
|
||||||
public final class MainAppView {
|
public final class MainAppView {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MainAppView.class);
|
||||||
|
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final VBox root = new VBox(16);
|
private final VBox root = new VBox(16);
|
||||||
private final Button themeToggleBtn = new Button();
|
private final Button themeToggleBtn = new Button();
|
||||||
|
|
||||||
// Callbacks das Ações
|
|
||||||
private Runnable onSignPdf;
|
private Runnable onSignPdf;
|
||||||
private Runnable onSignXml;
|
private Runnable onSignXml;
|
||||||
private Runnable onSignFile;
|
private Runnable onSignFile;
|
||||||
private Runnable onBatchSign;
|
private Runnable onBatchSign;
|
||||||
private Runnable onAttachedSign;
|
private Runnable onAttachedSign;
|
||||||
private Runnable onCoSign;
|
|
||||||
private Runnable onTimestamp;
|
|
||||||
private Runnable onLtvSign;
|
private Runnable onLtvSign;
|
||||||
private Runnable onValidateSignature;
|
private Runnable onValidateSignature;
|
||||||
private Runnable onValidateTimestamp;
|
|
||||||
private Runnable onManageSeals;
|
private Runnable onManageSeals;
|
||||||
private Runnable onExit;
|
private Runnable onExit;
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ public final class MainAppView {
|
|||||||
mainContent.setAlignment(Pos.CENTER);
|
mainContent.setAlignment(Pos.CENTER);
|
||||||
VBox.setVgrow(mainContent, Priority.ALWAYS);
|
VBox.setVgrow(mainContent, Priority.ALWAYS);
|
||||||
|
|
||||||
// --- Hub Central de Ações (Grid 3 colunas x 3 linhas) ---
|
// --- Hub Central de Ações (Grid 3 colunas x 2 linhas) ---
|
||||||
GridPane actionGrid = new GridPane();
|
GridPane actionGrid = new GridPane();
|
||||||
actionGrid.setHgap(12);
|
actionGrid.setHgap(12);
|
||||||
actionGrid.setVgap(12);
|
actionGrid.setVgap(12);
|
||||||
@@ -81,15 +81,10 @@ public final class MainAppView {
|
|||||||
actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0);
|
actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0);
|
||||||
actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0);
|
actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0);
|
||||||
|
|
||||||
// Linha 1: Formatos Avançados, Temporais e Identidade Visual
|
// Linha 1: Identidade Visual, Auditoria e Saída
|
||||||
actionGrid.add(createActionButton(SvgIcons.coSignIcon(), "Co-Assinar\nDocumento", () -> trigger(onCoSign)), 0, 1);
|
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 0, 1);
|
||||||
actionGrid.add(createActionButton(SvgIcons.timestampIcon(), "Carimbo\ndo Tempo", () -> trigger(onTimestamp)), 1, 1);
|
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 1, 1);
|
||||||
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 2, 1);
|
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 1);
|
||||||
|
|
||||||
// Linha 2: Auditoria, Validação e Saída
|
|
||||||
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 0, 2);
|
|
||||||
actionGrid.add(createActionButton(SvgIcons.validateTimestampIcon(), "Validar\nCarimbo", () -> trigger(onValidateTimestamp)), 1, 2);
|
|
||||||
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 2);
|
|
||||||
|
|
||||||
// --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) ---
|
// --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) ---
|
||||||
VBox sideColumn = new VBox(10);
|
VBox sideColumn = new VBox(10);
|
||||||
@@ -136,6 +131,14 @@ public final class MainAppView {
|
|||||||
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
||||||
).toExternalForm());
|
).toExternalForm());
|
||||||
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-effects")) {
|
||||||
|
logger.warn("Diagnóstico: Efeitos/sombras visuais DESATIVADOS via -Dcybercore.diag.no-effects=true");
|
||||||
|
var noEffectsUrl = getClass().getResource("/styles/no-effects.css");
|
||||||
|
if (noEffectsUrl != null) {
|
||||||
|
scene.getStylesheets().add(noEffectsUrl.toExternalForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configuração e inicialização do Tema
|
// Configuração e inicialização do Tema
|
||||||
ThemeManager.applyTheme(scene);
|
ThemeManager.applyTheme(scene);
|
||||||
updateThemeToggleButton();
|
updateThemeToggleButton();
|
||||||
@@ -144,11 +147,17 @@ public final class MainAppView {
|
|||||||
|
|
||||||
stage.setTitle(CyberCoreUiConstants.APP_NAME);
|
stage.setTitle(CyberCoreUiConstants.APP_NAME);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.setResizable(false);
|
|
||||||
stage.setMinWidth(780);
|
if (Boolean.getBoolean("cybercore.diag.plain-stage")) {
|
||||||
stage.setMinHeight(480);
|
logger.warn("Diagnóstico: Plain Stage ATIVADO via -Dcybercore.diag.plain-stage=true (janela redimensionável, sem restrições min/max)");
|
||||||
stage.setMaxWidth(820);
|
stage.setResizable(true);
|
||||||
stage.setMaxHeight(520);
|
} else {
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.setMinWidth(780);
|
||||||
|
stage.setMinHeight(480);
|
||||||
|
stage.setMaxWidth(820);
|
||||||
|
stage.setMaxHeight(520);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createHeaderBar() {
|
private HBox createHeaderBar() {
|
||||||
@@ -319,11 +328,8 @@ public final class MainAppView {
|
|||||||
public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; }
|
public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; }
|
||||||
public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; }
|
public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; }
|
||||||
public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; }
|
public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; }
|
||||||
public void setOnCoSign(Runnable onCoSign) { this.onCoSign = onCoSign; }
|
|
||||||
public void setOnTimestamp(Runnable onTimestamp) { this.onTimestamp = onTimestamp; }
|
|
||||||
public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; }
|
public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; }
|
||||||
public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; }
|
public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; }
|
||||||
public void setOnValidateTimestamp(Runnable onValidateTimestamp) { this.onValidateTimestamp = onValidateTimestamp; }
|
|
||||||
public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; }
|
public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; }
|
||||||
public void setOnExit(Runnable onExit) { this.onExit = onExit; }
|
public void setOnExit(Runnable onExit) { this.onExit = onExit; }
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,18 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
||||||
ManagedCertificate certificate, SigningContext context) {
|
ManagedCertificate certificate, SigningContext context) {
|
||||||
this(owner, applicationName, origin, certificate, context, null, null, false);
|
this(owner, applicationName, origin, certificate, context, null, null, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
||||||
ManagedCertificate certificate, SigningContext context,
|
ManagedCertificate certificate, SigningContext context,
|
||||||
String documentName, String documentType, boolean isAuthentication) {
|
String documentName, String documentType, boolean isAuthentication) {
|
||||||
|
this(owner, applicationName, origin, certificate, context, documentName, documentType, null, isAuthentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
||||||
|
ManagedCertificate certificate, SigningContext context,
|
||||||
|
String documentName, String documentType, Long documentSize, boolean isAuthentication) {
|
||||||
stage = new Stage();
|
stage = new Stage();
|
||||||
stage.initModality(Modality.APPLICATION_MODAL);
|
stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
@@ -52,11 +58,11 @@ public class SignaturePromptDialog {
|
|||||||
: "CyberCore Signer — Solicitação de Assinatura");
|
: "CyberCore Signer — Solicitação de Assinatura");
|
||||||
stage.setResizable(false);
|
stage.setResizable(false);
|
||||||
|
|
||||||
initLayout(applicationName, origin, certificate, context, documentName, documentType, isAuthentication);
|
initLayout(applicationName, origin, certificate, context, documentName, documentType, documentSize, isAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
|
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
|
||||||
String documentName, String documentType, boolean isAuthentication) {
|
String documentName, String documentType, Long documentSize, boolean isAuthentication) {
|
||||||
VBox root = new VBox(16);
|
VBox root = new VBox(16);
|
||||||
root.getStyleClass().add("dialog-container");
|
root.getStyleClass().add("dialog-container");
|
||||||
root.setPadding(new Insets(20, 24, 20, 24));
|
root.setPadding(new Insets(20, 24, 20, 24));
|
||||||
@@ -94,7 +100,8 @@ public class SignaturePromptDialog {
|
|||||||
grid.setVgap(10);
|
grid.setVgap(10);
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
addField(grid, row++, "Sistema:", appName + " (" + (origin != null ? origin : "Local") + ")");
|
addField(grid, row++, "Aplicação Solicitante:", appName);
|
||||||
|
addField(grid, row++, "Origem Web:", (origin != null && !origin.isBlank() ? origin : "Local"));
|
||||||
if (isAuthentication) {
|
if (isAuthentication) {
|
||||||
addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)");
|
addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)");
|
||||||
} else {
|
} else {
|
||||||
@@ -102,7 +109,12 @@ public class SignaturePromptDialog {
|
|||||||
String docLabel = (documentType != null && !documentType.isBlank())
|
String docLabel = (documentType != null && !documentType.isBlank())
|
||||||
? "[" + documentType.toUpperCase() + "] " + documentName
|
? "[" + documentType.toUpperCase() + "] " + documentName
|
||||||
: documentName;
|
: documentName;
|
||||||
|
if (documentSize != null && documentSize > 0) {
|
||||||
|
docLabel += " (" + formatFileSize(documentSize) + ")";
|
||||||
|
}
|
||||||
addField(grid, row++, "Documento:", docLabel);
|
addField(grid, row++, "Documento:", docLabel);
|
||||||
|
} else if (documentSize != null && documentSize > 0) {
|
||||||
|
addField(grid, row++, "Tamanho do Documento:", formatFileSize(documentSize));
|
||||||
}
|
}
|
||||||
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
|
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
|
||||||
}
|
}
|
||||||
@@ -128,14 +140,16 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
detailsBox.getChildren().add(grid);
|
detailsBox.getChildren().add(grid);
|
||||||
|
|
||||||
// Caixa do Hash / Digest
|
// Caixa do Hash / Digest formatado em blocos legíveis (WYSIWYS)
|
||||||
VBox hashBox = new VBox(6);
|
VBox hashBox = new VBox(6);
|
||||||
Label hashLabel = new Label("Hash do Documento (SHA-256 Digest):");
|
Label hashLabel = new Label("Hash do Documento (SHA-256 Digest formatado):");
|
||||||
hashLabel.getStyleClass().add("dialog-field-title");
|
hashLabel.getStyleClass().add("dialog-field-title");
|
||||||
|
|
||||||
String hashHex = HexFormat.of().formatHex(context.digest());
|
String hashHex = HexFormat.of().formatHex(context.digest());
|
||||||
Label hashValue = new Label(hashHex);
|
String formattedHash = formatHashBlocks(hashHex);
|
||||||
|
Label hashValue = new Label(formattedHash);
|
||||||
hashValue.getStyleClass().add("dialog-hash-value");
|
hashValue.getStyleClass().add("dialog-hash-value");
|
||||||
|
hashValue.setStyle("-fx-font-family: 'Liberation Mono', 'Courier New', monospace; -fx-font-size: 11px;");
|
||||||
hashValue.setWrapText(true);
|
hashValue.setWrapText(true);
|
||||||
|
|
||||||
hashBox.getChildren().addAll(hashLabel, hashValue);
|
hashBox.getChildren().addAll(hashLabel, hashValue);
|
||||||
@@ -149,7 +163,7 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
boolean isA3 = cert.type() == CertificateType.A3;
|
boolean isA3 = cert.type() == CertificateType.A3;
|
||||||
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
|
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
|
||||||
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(cert.id());
|
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(context != null ? context.applicationId() : null, cert.id());
|
||||||
|
|
||||||
if (isA3) {
|
if (isA3) {
|
||||||
pinContainer.getStyleClass().add("dialog-box");
|
pinContainer.getStyleClass().add("dialog-box");
|
||||||
@@ -255,9 +269,9 @@ public class SignaturePromptDialog {
|
|||||||
if (pinProvided) {
|
if (pinProvided) {
|
||||||
char[] pinChars = typedPin.toCharArray();
|
char[] pinChars = typedPin.toCharArray();
|
||||||
if (cbSavePin.isSelected()) {
|
if (cbSavePin.isSelected()) {
|
||||||
PinSessionManager.getInstance().savePin(cert.id(), pinChars);
|
PinSessionManager.getInstance().savePin(context != null ? context.applicationId() : null, cert.id(), pinChars);
|
||||||
} else {
|
} else {
|
||||||
PinSessionManager.getInstance().clearPin(cert.id());
|
PinSessionManager.getInstance().clearPin(context != null ? context.applicationId() : null, cert.id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,4 +315,27 @@ public class SignaturePromptDialog {
|
|||||||
stage.toFront();
|
stage.toFront();
|
||||||
return approvalFuture;
|
return approvalFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String formatFileSize(long bytes) {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return bytes + " B";
|
||||||
|
} else if (bytes < 1024 * 1024) {
|
||||||
|
return String.format(java.util.Locale.ROOT, "%.1f KB", bytes / 1024.0);
|
||||||
|
} else {
|
||||||
|
return String.format(java.util.Locale.ROOT, "%.1f MB", bytes / (1024.0 * 1024.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatHashBlocks(String hex) {
|
||||||
|
if (hex == null || hex.isBlank()) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String upper = hex.toUpperCase(java.util.Locale.ROOT);
|
||||||
|
for (int i = 0; i < upper.length(); i++) {
|
||||||
|
if (i > 0 && i % 4 == 0) {
|
||||||
|
sb.append(' ');
|
||||||
|
}
|
||||||
|
sb.append(upper.charAt(i));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ public final class SignatureValidationDialog {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class);
|
private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class);
|
||||||
|
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final boolean isTimestamp;
|
|
||||||
private final SignatureValidationService validationService = new SignatureValidationService();
|
private final SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
|
||||||
private RadioButton rbAttached;
|
private RadioButton rbAttached;
|
||||||
@@ -58,15 +57,13 @@ public final class SignatureValidationDialog {
|
|||||||
private Button btnSaveExtracted;
|
private Button btnSaveExtracted;
|
||||||
private byte[] lastExtractedContent;
|
private byte[] lastExtractedContent;
|
||||||
|
|
||||||
public SignatureValidationDialog(Stage owner, boolean isTimestamp) {
|
public SignatureValidationDialog(Stage owner) {
|
||||||
this.isTimestamp = isTimestamp;
|
|
||||||
|
|
||||||
this.stage = new Stage();
|
this.stage = new Stage();
|
||||||
this.stage.initModality(Modality.APPLICATION_MODAL);
|
this.stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
this.stage.initOwner(owner);
|
this.stage.initOwner(owner);
|
||||||
}
|
}
|
||||||
this.stage.setTitle(isTimestamp ? "CyberCore Signer — Validação de Carimbo do Tempo" : "CyberCore Signer — Validação de Assinatura Digital");
|
this.stage.setTitle("CyberCore Signer — Validação de Assinatura Digital");
|
||||||
this.stage.setResizable(true);
|
this.stage.setResizable(true);
|
||||||
this.stage.setMinWidth(680);
|
this.stage.setMinWidth(680);
|
||||||
this.stage.setMinHeight(520);
|
this.stage.setMinHeight(520);
|
||||||
@@ -82,11 +79,10 @@ public final class SignatureValidationDialog {
|
|||||||
HBox header = new HBox(12);
|
HBox header = new HBox(12);
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
header.getChildren().addAll(
|
header.getChildren().addAll(
|
||||||
isTimestamp ? SvgIcons.validateTimestampIcon() : SvgIcons.validateIcon(),
|
SvgIcons.validateIcon(),
|
||||||
createHeaderLabel(
|
createHeaderLabel(
|
||||||
isTimestamp ? "VALIDAÇÃO DE CARIMBO DO TEMPO" : "VALIDAÇÃO DE ASSINATURA DIGITAL",
|
"VALIDAÇÃO DE ASSINATURA DIGITAL",
|
||||||
isTimestamp ? "Verifique a autenticidade e conformidade RFC 3161 TSP."
|
"Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
|
||||||
: "Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,11 +106,6 @@ public final class SignatureValidationDialog {
|
|||||||
radioRow.getChildren().addAll(rbAttached, rbDetached);
|
radioRow.getChildren().addAll(rbAttached, rbDetached);
|
||||||
typeBox.getChildren().addAll(lblTypeTitle, radioRow);
|
typeBox.getChildren().addAll(lblTypeTitle, radioRow);
|
||||||
|
|
||||||
if (isTimestamp) {
|
|
||||||
typeBox.setVisible(false);
|
|
||||||
typeBox.setManaged(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Arquivo de Assinatura / Arquivo Assinado
|
// 1. Arquivo de Assinatura / Arquivo Assinado
|
||||||
VBox sigBox = new VBox(8);
|
VBox sigBox = new VBox(8);
|
||||||
sigBox.getStyleClass().add("dialog-box");
|
sigBox.getStyleClass().add("dialog-box");
|
||||||
@@ -197,7 +188,7 @@ public final class SignatureValidationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateModeView() {
|
private void updateModeView() {
|
||||||
boolean isAttached = rbAttached.isSelected() && !isTimestamp;
|
boolean isAttached = rbAttached.isSelected();
|
||||||
docBox.setVisible(!isAttached);
|
docBox.setVisible(!isAttached);
|
||||||
docBox.setManaged(!isAttached);
|
docBox.setManaged(!isAttached);
|
||||||
|
|
||||||
@@ -205,7 +196,7 @@ public final class SignatureValidationDialog {
|
|||||||
lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
|
lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
|
||||||
txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos...");
|
txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos...");
|
||||||
} else {
|
} else {
|
||||||
lblSigPath.setText(isTimestamp ? "Arquivo de Carimbo (.tsr, .tsp, .p7s):" : "Arquivo de Assinatura (.p7s, .sig):");
|
lblSigPath.setText("Arquivo de Assinatura (.p7s, .sig):");
|
||||||
txtSigPath.setPromptText("Selecione o arquivo de assinatura digital...");
|
txtSigPath.setPromptText("Selecione o arquivo de assinatura digital...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +218,7 @@ public final class SignatureValidationDialog {
|
|||||||
if (rbAttached.isSelected()) {
|
if (rbAttached.isSelected()) {
|
||||||
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*"));
|
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*"));
|
||||||
} else {
|
} else {
|
||||||
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig, *.tsr)", "*.p7s", "*.sig", "*.tsr", "*.*"));
|
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig)", "*.p7s", "*.sig", "*.*"));
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = ch.showOpenDialog(stage);
|
File f = ch.showOpenDialog(stage);
|
||||||
@@ -249,7 +240,7 @@ public final class SignatureValidationDialog {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isDetachedMode = rbDetached.isSelected() || isTimestamp;
|
boolean isDetachedMode = rbDetached.isSelected();
|
||||||
if (isDetachedMode && (docFile == null || !docFile.exists())) {
|
if (isDetachedMode && (docFile == null || !docFile.exists())) {
|
||||||
showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado",
|
showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado",
|
||||||
"Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura.");
|
"Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura.");
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- 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"/>
|
<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 -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" 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="13" y1="22" x2="35" y2="22" 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"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta vermelha com sigla PDF -->
|
<!-- Etiqueta vermelha com sigla PDF perfeitamente centralizada -->
|
||||||
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="#d32f2f"/>
|
||||||
<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>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8.5px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">PDF</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 996 B After Width: | Height: | Size: 1.0 KiB |
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- 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"/>
|
<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 -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" 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="13" y1="22" x2="35" y2="22" 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"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta azul com sigla XML -->
|
<!-- Etiqueta azul com sigla XML perfeitamente centralizada -->
|
||||||
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
|
<rect x="11" y="31" 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>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central"><XML></text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
/* CyberCore Signer - Modo Diagnóstico: Remoção de sombras/efeitos visuais dinâmicos */
|
||||||
|
|
||||||
|
.cyber-action-card,
|
||||||
|
.serpro-action-btn,
|
||||||
|
.cyber-side-card,
|
||||||
|
.serpro-side-btn,
|
||||||
|
.cyber-header-bar,
|
||||||
|
.theme-toggle-btn,
|
||||||
|
.cyber-status-badge {
|
||||||
|
-fx-effect: null !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .cyber-action-card:hover,
|
||||||
|
.theme-light .serpro-action-btn:hover,
|
||||||
|
.theme-dark .cyber-action-card:hover,
|
||||||
|
.theme-dark .serpro-action-btn:hover,
|
||||||
|
.cyber-side-card:hover,
|
||||||
|
.serpro-side-btn:hover,
|
||||||
|
.theme-toggle-btn:hover {
|
||||||
|
-fx-effect: null !important;
|
||||||
|
}
|
||||||
@@ -55,4 +55,22 @@ class AssetManagerTest {
|
|||||||
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
|
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
|
||||||
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
|
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve interpretar atributos style inline no padrão de exportação do Inkscape/Figma")
|
||||||
|
void shouldParseSvgWithInlineStyles() {
|
||||||
|
String svgWithStyle = """
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<rect x="10" y="10" width="28" height="28" style="fill:#d32f2f;stroke:#0288d1;stroke-width:2.5;opacity:0.9"/>
|
||||||
|
<text x="24" y="24" style="fill:#ffffff;font-size:10px">OK</text>
|
||||||
|
</svg>
|
||||||
|
""";
|
||||||
|
try (var in = new java.io.ByteArrayInputStream(svgWithStyle.getBytes(java.nio.charset.StandardCharsets.UTF_8))) {
|
||||||
|
Node node = AssetManager.parseSvgToNode(in, 48, 48);
|
||||||
|
assertThat(node).isNotNull();
|
||||||
|
assertThat(node).isInstanceOf(javafx.scene.layout.Pane.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
org.junit.jupiter.api.Assertions.fail("Falha ao analisar SVG com estilo inline: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package br.com.cybercore.signer.ui.tray;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.awt.SystemTray;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DisplayName("Testes do Gerenciador de Bandeja (SystemTrayManager)")
|
||||||
|
class SystemTrayManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve rejeitar Stage nulo no construtor")
|
||||||
|
void shouldRejectNullStage() {
|
||||||
|
assertThatThrownBy(() -> new SystemTrayManager(null, () -> {}))
|
||||||
|
.isInstanceOf(NullPointerException.class)
|
||||||
|
.hasMessageContaining("stage must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve reportar suporte do ambiente ou lidar graciosamente sem UI nativa")
|
||||||
|
void shouldReportSupportOrHandleGracefully() {
|
||||||
|
boolean supported = SystemTray.isSupported();
|
||||||
|
// Verifica que chamada não lança exceção fatal
|
||||||
|
assertThat(supported).isIn(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
<spotbugs-maven-plugin.version>4.10.4.1</spotbugs-maven-plugin.version>
|
<spotbugs-maven-plugin.version>4.10.4.1</spotbugs-maven-plugin.version>
|
||||||
<findsecbugs-plugin.version>1.14.0</findsecbugs-plugin.version>
|
<findsecbugs-plugin.version>1.14.0</findsecbugs-plugin.version>
|
||||||
<javafx-maven-plugin.version>0.0.8</javafx-maven-plugin.version>
|
<javafx-maven-plugin.version>0.0.8</javafx-maven-plugin.version>
|
||||||
|
<cyclonedx-maven-plugin.version>2.9.1</cyclonedx-maven-plugin.version>
|
||||||
|
|
||||||
<!-- Qualidade -->
|
<!-- Qualidade -->
|
||||||
<spotbugs.skip>false</spotbugs.skip>
|
<spotbugs.skip>false</spotbugs.skip>
|
||||||
@@ -315,6 +316,21 @@
|
|||||||
<artifactId>javafx-maven-plugin</artifactId>
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
<version>${javafx-maven-plugin.version}</version>
|
<version>${javafx-maven-plugin.version}</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.cyclonedx</groupId>
|
||||||
|
<artifactId>cyclonedx-maven-plugin</artifactId>
|
||||||
|
<version>${cyclonedx-maven-plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<projectType>application</projectType>
|
||||||
|
<schemaVersion>1.6</schemaVersion>
|
||||||
|
<includeBomSerialNumber>true</includeBomSerialNumber>
|
||||||
|
<includeCompileScope>true</includeCompileScope>
|
||||||
|
<includeProvidedScope>true</includeProvidedScope>
|
||||||
|
<includeRuntimeScope>true</includeRuntimeScope>
|
||||||
|
<includeSystemScope>false</includeSystemScope>
|
||||||
|
<includeTestScope>false</includeTestScope>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</pluginManagement>
|
</pluginManagement>
|
||||||
|
|
||||||
@@ -377,6 +393,19 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.cyclonedx</groupId>
|
||||||
|
<artifactId>cyclonedx-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>cyclonedx-aggregate-bom</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>makeAggregateBom</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"description": "Configuração do Renovate para o CyberCore Signer com suporte ao Gitea",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"semanticCommits": "enabled",
|
||||||
|
"commitMessagePrefix": "chore(deps):",
|
||||||
|
"timezone": "America/Sao_Paulo",
|
||||||
|
"rebaseWhen": "auto",
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Agrupamento de bibliotecas criptográficas Bouncy Castle",
|
||||||
|
"groupName": "Bouncy Castle",
|
||||||
|
"matchPackagePatterns": [
|
||||||
|
"^org\\.bouncycastle:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Agrupamento do framework de renderização e manipulação de PDF Apache PDFBox",
|
||||||
|
"groupName": "Apache PDFBox",
|
||||||
|
"matchPackagePatterns": [
|
||||||
|
"^org\\.apache\\.pdfbox:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Agrupamento de serialização e parsing JSON Jackson",
|
||||||
|
"groupName": "Jackson",
|
||||||
|
"matchPackagePatterns": [
|
||||||
|
"^com\\.fasterxml\\.jackson(\\..*)?:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Agrupamento dos módulos da interface gráfica JavaFX (OpenJFX)",
|
||||||
|
"groupName": "JavaFX",
|
||||||
|
"matchPackagePatterns": [
|
||||||
|
"^org\\.openjfx:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Agrupamento de frameworks de teste (JUnit, AssertJ, Mockito, ArchUnit)",
|
||||||
|
"groupName": "Test Dependencies",
|
||||||
|
"matchPackagePatterns": [
|
||||||
|
"^org\\.junit",
|
||||||
|
"^org\\.assertj:",
|
||||||
|
"^org\\.mockito:",
|
||||||
|
"^com\\.tngtech\\.archunit:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vulnerabilityAlerts": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package scripts.bench;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSealPosition;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSigningService;
|
||||||
|
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||||
|
import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||||
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
|
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilitário de benchmark para medição de latência de certificados e assinatura PAdES (100KB, 5MB e 50MB).
|
||||||
|
*/
|
||||||
|
public class BenchmarkRunner {
|
||||||
|
|
||||||
|
private static final String TARGET_DIR = "target/bench-data";
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
System.out.println("===============================================================");
|
||||||
|
System.out.println(" CyberCore Signer — Benchmark de Desempenho (Certificados & PAdES)");
|
||||||
|
System.out.println("===============================================================");
|
||||||
|
|
||||||
|
File dir = new File(TARGET_DIR);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Prepara Certificado A1 de Teste
|
||||||
|
System.out.print("[1/3] Inicializando provedor e certificado A1 de teste... ");
|
||||||
|
long tCertGenStart = System.currentTimeMillis();
|
||||||
|
CertificateManager certManager = new CertificateManager();
|
||||||
|
Pkcs12CertificateProvider a1Provider = new Pkcs12CertificateProvider();
|
||||||
|
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
KeyPair keyPair = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
X500Name name = new X500Name("CN=BENCHMARK TESTE:12345678900, O=ICP-Brasil Teste, C=BR");
|
||||||
|
Date notBefore = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
|
||||||
|
Date notAfter = Date.from(Instant.now().plus(30, ChronoUnit.DAYS));
|
||||||
|
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(
|
||||||
|
name, BigInteger.ONE, notBefore, notAfter, name,
|
||||||
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
|
||||||
|
);
|
||||||
|
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
|
||||||
|
builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation));
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
|
||||||
|
X509CertificateHolder holder = builder.build(signer);
|
||||||
|
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(holder);
|
||||||
|
|
||||||
|
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||||
|
ks.load(null, null);
|
||||||
|
ks.setKeyEntry("bench-alias", keyPair.getPrivate(), "1234".toCharArray(), new Certificate[]{cert});
|
||||||
|
ByteArrayOutputStream baosKs = new ByteArrayOutputStream();
|
||||||
|
ks.store(baosKs, "1234".toCharArray());
|
||||||
|
a1Provider.loadFromBytes(baosKs.toByteArray(), "1234".toCharArray());
|
||||||
|
certManager.registerProvider(a1Provider);
|
||||||
|
System.out.println("OK (" + (System.currentTimeMillis() - tCertGenStart) + " ms)");
|
||||||
|
|
||||||
|
// 2. Mede Tempo de Enumeração de Certificados
|
||||||
|
System.out.print("[2/3] Medindo tempo de enumeração de certificados (discoverCertificates)... ");
|
||||||
|
long tDiscStart = System.nanoTime();
|
||||||
|
List<ManagedCertificate> certs = certManager.discoverCertificates();
|
||||||
|
long tDiscEnd = System.nanoTime();
|
||||||
|
double discMs = (tDiscEnd - tDiscStart) / 1_000_000.0;
|
||||||
|
System.out.printf("OK (%.2f ms | %d certificado(s) listado(s))\n", discMs, certs.size());
|
||||||
|
|
||||||
|
String certId = certs.get(0).id();
|
||||||
|
|
||||||
|
// 3. Prepara arquivos PDF de 100 KB, 5 MB e 50 MB
|
||||||
|
System.out.println("[3/3] Preparando e assinando documentos PDF com PAdES e selo visual...");
|
||||||
|
File pdf100Kb = preparePdf(new File(dir, "doc_100kb.pdf"), 100 * 1024);
|
||||||
|
File pdf5Mb = preparePdf(new File(dir, "doc_5mb.pdf"), 5 * 1024 * 1024);
|
||||||
|
File pdf50Mb = preparePdf(new File(dir, "doc_50mb.pdf"), 50 * 1024 * 1024);
|
||||||
|
|
||||||
|
PdfSigningService pdfService = new PdfSigningService(certManager);
|
||||||
|
PdfSignatureOptions options = PdfSignatureOptions.builder(certId)
|
||||||
|
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.reason("Homologacao de Performance")
|
||||||
|
.location("Brasilia, DF")
|
||||||
|
.pinOrPassword("1234".toCharArray())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Warmup JIT (PDF pequeno)
|
||||||
|
pdfService.signPdf(Files.readAllBytes(pdf100Kb.toPath()), options);
|
||||||
|
|
||||||
|
// Medição 100 KB
|
||||||
|
long t100Start = System.nanoTime();
|
||||||
|
byte[] signed100 = pdfService.signPdf(Files.readAllBytes(pdf100Kb.toPath()), options);
|
||||||
|
long t100End = System.nanoTime();
|
||||||
|
double ms100 = (t100End - t100Start) / 1_000_000.0;
|
||||||
|
|
||||||
|
// Medição 5 MB
|
||||||
|
long t5mStart = System.nanoTime();
|
||||||
|
byte[] signed5m = pdfService.signPdf(Files.readAllBytes(pdf5Mb.toPath()), options);
|
||||||
|
long t5mEnd = System.nanoTime();
|
||||||
|
double ms5m = (t5mEnd - t5mStart) / 1_000_000.0;
|
||||||
|
|
||||||
|
// Medição 50 MB
|
||||||
|
long t50mStart = System.nanoTime();
|
||||||
|
byte[] signed50m = pdfService.signPdf(Files.readAllBytes(pdf50Mb.toPath()), options);
|
||||||
|
long t50mEnd = System.nanoTime();
|
||||||
|
double ms50m = (t50mEnd - t50mStart) / 1_000_000.0;
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("==================== RESULTADOS DE LATÊNCIA PAdES ====================");
|
||||||
|
System.out.printf(" - Enumeração de Certificados: %.2f ms\n", discMs);
|
||||||
|
System.out.printf(" - Assinatura PAdES 100 KB: %.2f ms (saída: %d KB)\n", ms100, signed100.length / 1024);
|
||||||
|
System.out.printf(" - Assinatura PAdES 5 MB: %.2f ms (saída: %.2f MB)\n", ms5m, signed5m.length / (1024.0 * 1024.0));
|
||||||
|
System.out.printf(" - Assinatura PAdES 50 MB: %.2f ms (saída: %.2f MB)\n", ms50m, signed50m.length / (1024.0 * 1024.0));
|
||||||
|
System.out.println("=======================================================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File preparePdf(File file, int targetSizeBytes) throws Exception {
|
||||||
|
if (file.exists() && file.length() >= targetSizeBytes * 0.95) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
System.out.printf(" -> Gerando %s (~%d KB)... ", file.getName(), targetSizeBytes / 1024);
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage page = new PDPage();
|
||||||
|
doc.addPage(page);
|
||||||
|
|
||||||
|
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 14);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("CyberCore Signer — Documento de Benchmark de Performance");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para arquivos de 5MB e 50MB, adiciona páginas e streams para atingir a meta de tamanho
|
||||||
|
int approxPageSize = 2500;
|
||||||
|
int pagesNeeded = Math.max(1, targetSizeBytes / approxPageSize);
|
||||||
|
for (int p = 1; p < pagesNeeded && p < 1200; p++) {
|
||||||
|
PDPage extraPage = new PDPage();
|
||||||
|
doc.addPage(extraPage);
|
||||||
|
try (PDPageContentStream cs = new PDPageContentStream(doc, extraPage)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 10);
|
||||||
|
cs.newLineAtOffset(50, 700);
|
||||||
|
cs.showText("Pagina de teste de volume " + p + " com metadados para teste PAdES.");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grava documento
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||||
|
doc.save(fos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ainda não atingiu o tamanho exato desejado (especialmente para 50MB), preenche comentários PDF (% padding)
|
||||||
|
long currentLen = file.length();
|
||||||
|
if (currentLen < targetSizeBytes) {
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(file, true)) {
|
||||||
|
byte[] chunk = new byte[64 * 1024];
|
||||||
|
java.util.Arrays.fill(chunk, (byte) 'A');
|
||||||
|
chunk[0] = '%';
|
||||||
|
chunk[chunk.length - 1] = '\n';
|
||||||
|
while (file.length() < targetSizeBytes) {
|
||||||
|
long remaining = targetSizeBytes - file.length();
|
||||||
|
int toWrite = (int) Math.min(chunk.length, remaining);
|
||||||
|
fos.write(chunk, 0, toWrite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.printf("OK (%d KB)\n", file.length() / 1024);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Script de medição de tempo de inicialização e memória RSS em idle do CyberCore Signer
|
||||||
|
PORT=21325
|
||||||
|
JAR_FILE="modules/cybercore-app/target/cybercore-signer.jar"
|
||||||
|
JAVA_BIN="${JAVA_HOME:-/home/claudecio/.jdks/jdk-25.0.4.1+1}/bin/java"
|
||||||
|
|
||||||
|
if [ ! -f "$JAR_FILE" ]; then
|
||||||
|
echo "Erro: JAR $JAR_FILE não encontrado. Execute ./mvnw package primeiro."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Garante que a porta esteja livre antes de iniciar
|
||||||
|
fuser -k "${PORT}/tcp" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Timestamp inicial em milissegundos
|
||||||
|
START_TIME=$(date +%s%3N)
|
||||||
|
|
||||||
|
# Inicia o agente em modo headless
|
||||||
|
"$JAVA_BIN" -jar "$JAR_FILE" --headless --port "$PORT" > /dev/null 2>&1 &
|
||||||
|
APP_PID=$!
|
||||||
|
|
||||||
|
# Aguarda até que o endpoint CCSP /status responda HTTP 200
|
||||||
|
MAX_WAIT_SECONDS=15
|
||||||
|
READY=0
|
||||||
|
while true; do
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT}/ccsp/v1/status" 2>/dev/null || echo "000")
|
||||||
|
if [ "$HTTP_CODE" = "200" ]; then
|
||||||
|
READY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
CURRENT_TIME=$(date +%s%3N)
|
||||||
|
ELAPSED=$((CURRENT_TIME - START_TIME))
|
||||||
|
if [ "$ELAPSED" -gt $((MAX_WAIT_SECONDS * 1000)) ]; then
|
||||||
|
echo "Timeout aguardando inicialização do servidor CCSP."
|
||||||
|
kill -9 "$APP_PID" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
|
||||||
|
END_TIME=$(date +%s%3N)
|
||||||
|
STARTUP_MS=$((END_TIME - START_TIME))
|
||||||
|
|
||||||
|
# Aguarda 2 segundos para estabilização de memória em repouso (idle)
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Mede RSS via ps (em KB)
|
||||||
|
RSS_KB=$(ps -o rss= -p "$APP_PID" | tr -d ' ' || echo "0")
|
||||||
|
RSS_MB=$(awk "BEGIN {printf \"%.2f\", $RSS_KB / 1024}")
|
||||||
|
|
||||||
|
# Encerra o processo limpo
|
||||||
|
kill "$APP_PID" 2>/dev/null || true
|
||||||
|
wait "$APP_PID" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "STARTUP_MS=${STARTUP_MS}"
|
||||||
|
echo "RSS_KB=${RSS_KB}"
|
||||||
|
echo "RSS_MB=${RSS_MB}"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Script mestre de execução e consolidação de benchmarks de performance do CyberCore Signer
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
JAVA_BIN="${JAVA_HOME:-/home/claudecio/.jdks/jdk-25.0.4.1+1}/bin/java"
|
||||||
|
JAVAC_BIN="${JAVA_HOME:-/home/claudecio/.jdks/jdk-25.0.4.1+1}/bin/javac"
|
||||||
|
JAR_FILE="modules/cybercore-app/target/cybercore-signer.jar"
|
||||||
|
|
||||||
|
echo "======================================================================="
|
||||||
|
echo " CYBERCORE SIGNER — SUÍTE DE MEDIÇÃO DE PERFORMANCE"
|
||||||
|
echo "======================================================================="
|
||||||
|
echo "Ambiente: $(uname -s) $(uname -m)"
|
||||||
|
echo "Java: $("$JAVA_BIN" -version 2>&1 | head -n 1)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 1. Medições de Tempo de Inicialização e RSS (Média de 3 execuções)
|
||||||
|
echo "-----------------------------------------------------------------------"
|
||||||
|
echo "[1/2] Medindo Tempo de Inicialização do Servidor CCSP e Memória RSS..."
|
||||||
|
echo "-----------------------------------------------------------------------"
|
||||||
|
|
||||||
|
STARTUP_SUM=0
|
||||||
|
RSS_SUM=0
|
||||||
|
RUNS=3
|
||||||
|
|
||||||
|
for i in $(seq 1 $RUNS); do
|
||||||
|
echo -n " -> Execução $i de $RUNS: "
|
||||||
|
OUT=$(./scripts/bench/bench_startup_rss.sh)
|
||||||
|
S_MS=$(echo "$OUT" | grep "STARTUP_MS=" | cut -d'=' -f2)
|
||||||
|
R_MB=$(echo "$OUT" | grep "RSS_MB=" | cut -d'=' -f2)
|
||||||
|
echo "Startup = ${S_MS} ms | RSS = ${R_MB} MB"
|
||||||
|
STARTUP_SUM=$((STARTUP_SUM + S_MS))
|
||||||
|
RSS_SUM=$(awk "BEGIN {print $RSS_SUM + $R_MB}")
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
AVG_STARTUP=$((STARTUP_SUM / RUNS))
|
||||||
|
AVG_RSS=$(awk "BEGIN {printf \"%.2f\", $RSS_SUM / $RUNS}")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=> Média de Inicialização CCSP: ${AVG_STARTUP} ms"
|
||||||
|
echo "=> Média de Memória (RSS) em Idle: ${AVG_RSS} MB"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 2. Compilação e Execução do BenchmarkRunner (Certificados e PAdES)
|
||||||
|
echo "-----------------------------------------------------------------------"
|
||||||
|
echo "[2/2] Medindo Enumeração de Certificados e Latência PAdES..."
|
||||||
|
echo "-----------------------------------------------------------------------"
|
||||||
|
|
||||||
|
mkdir -p target/bench-classes
|
||||||
|
"$JAVAC_BIN" -cp "$JAR_FILE" -d target/bench-classes scripts/bench/BenchmarkRunner.java
|
||||||
|
"$JAVA_BIN" -cp "target/bench-classes:$JAR_FILE" scripts.bench.BenchmarkRunner
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# CyberCore Signer - Diagnóstico e Monitor de Janela Win32 / DWM
|
||||||
|
# Suporta matar instâncias anteriores órfãs e monitorar a janela real.
|
||||||
|
# Uso simples:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
|
||||||
|
# Ou iniciando o app automaticamente junto com o monitor:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1 -AutoLaunch
|
||||||
|
# ==============================================================================
|
||||||
|
param (
|
||||||
|
[switch]$AutoLaunch,
|
||||||
|
[string]$ExtraArgs = "--dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " CyberCore Signer - Monitor de Janela Win32 / DWM" -ForegroundColor Cyan
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 1. Encerra processos java zumbis anteriores que possam estar travando a porta 21325 ou a bandeja
|
||||||
|
$oldProcs = Get-Process -Name java -ErrorAction SilentlyContinue
|
||||||
|
if ($oldProcs) {
|
||||||
|
Write-Host "[INFO] Encerrando $($oldProcs.Count) processo(s) Java em segundo plano para limpar o ambiente..." -ForegroundColor Yellow
|
||||||
|
$oldProcs | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Start-Sleep -Milliseconds 800
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = @"
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public class Win32WindowWatcher {
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT {
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsIconic(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
|
[DllImport("dwmapi.dll")]
|
||||||
|
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
||||||
|
|
||||||
|
public const int DWMWA_CLOAKED = 14;
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
Add-Type -TypeDefinition $signature -PassThru | Out-Null
|
||||||
|
|
||||||
|
if ($AutoLaunch) {
|
||||||
|
Write-Host "[LAUNCH] Iniciando cybercore-signer.jar com argumentos: $ExtraArgs" -ForegroundColor Green
|
||||||
|
$jarPath = ".\cybercore-signer.jar"
|
||||||
|
if (-not (Test-Path $jarPath)) {
|
||||||
|
$jarPath = ".\modules\cybercore-app\target\cybercore-signer.jar"
|
||||||
|
}
|
||||||
|
Start-Process -FilePath "java" -ArgumentList "-jar", $jarPath, $ExtraArgs -PassThru | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Aguardando janela principal abrir (verificando processos e Win32)..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$lastState = ""
|
||||||
|
$found = $false
|
||||||
|
$monitoredHwnd = [IntPtr]::Zero
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
# Procura via FindWindow pelo título ou pelo MainWindowHandle do processo Java
|
||||||
|
$hWnd = [Win32WindowWatcher]::FindWindow($null, "CyberCore Signer")
|
||||||
|
|
||||||
|
if ($hWnd -eq [IntPtr]::Zero) {
|
||||||
|
$javaProcs = Get-Process -Name java -ErrorAction SilentlyContinue
|
||||||
|
foreach ($p in $javaProcs) {
|
||||||
|
if ($p.MainWindowHandle -ne [IntPtr]::Zero) {
|
||||||
|
$hWnd = $p.MainWindowHandle
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hWnd -ne [IntPtr]::Zero -and [Win32WindowWatcher]::IsWindow($hWnd)) {
|
||||||
|
if (-not $found -or $hWnd -ne $monitoredHwnd) {
|
||||||
|
$found = $true
|
||||||
|
$monitoredHwnd = $hWnd
|
||||||
|
Write-Host "[OK] JANELA DETECTADA! HWND: $hWnd. Monitorando em tempo real a cada 200ms..." -ForegroundColor Green
|
||||||
|
Write-Host ">>> Passe o mouse sobre a janela agora para observar o que o Windows faz. <<<" -ForegroundColor Magenta
|
||||||
|
}
|
||||||
|
|
||||||
|
$visible = [Win32WindowWatcher]::IsWindowVisible($hWnd)
|
||||||
|
$iconic = [Win32WindowWatcher]::IsIconic($hWnd)
|
||||||
|
$foreground = ([Win32WindowWatcher]::GetForegroundWindow() -eq $hWnd)
|
||||||
|
|
||||||
|
$rect = New-Object Win32WindowWatcher+RECT
|
||||||
|
[Win32WindowWatcher]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
|
||||||
|
$w = $rect.Right - $rect.Left
|
||||||
|
$h = $rect.Bottom - $rect.Top
|
||||||
|
|
||||||
|
$cloakedVal = 0
|
||||||
|
[Win32WindowWatcher]::DwmGetWindowAttribute($hWnd, [Win32WindowWatcher]::DWMWA_CLOAKED, [ref]$cloakedVal, 4) | Out-Null
|
||||||
|
$cloaked = ($cloakedVal -ne 0)
|
||||||
|
|
||||||
|
$currentState = "Visible=$visible | Cloaked(DWM)=$cloaked | Minimized=$iconic | Focused=$foreground | Rect=($($rect.Left),$($rect.Top)) ${w}x${h}"
|
||||||
|
|
||||||
|
if ($currentState -ne $lastState) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
|
||||||
|
if (-not $visible -or $cloaked -or $w -le 0 -or $h -le 0) {
|
||||||
|
Write-Host "[$timestamp] [ALERTA-JANELA-SUMIU] $currentState" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "[$timestamp] [STATUS] $currentState" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
$lastState = $currentState
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($found) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
|
||||||
|
Write-Host "[$timestamp] [DESTRUÍDA/FECHADA] O handle da janela não existe mais no Windows!" -ForegroundColor Red
|
||||||
|
$found = $false
|
||||||
|
$lastState = ""
|
||||||
|
$monitoredHwnd = [IntPtr]::Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ cd "${PROJECT_ROOT}"
|
|||||||
if [ -n "${JAVA_HOME:-}" ]; then
|
if [ -n "${JAVA_HOME:-}" ]; then
|
||||||
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
||||||
else
|
else
|
||||||
export JAVA_HOME="/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
export JAVA_HOME="${JAVA_HOME:-${HOME}/.jdks/jdk-25.0.4.1+1}"
|
||||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||||
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,42 +1,85 @@
|
|||||||
# @cybercore/signer-sdk
|
# @cybercore/signer-sdk
|
||||||
|
|
||||||
SDK oficial em JavaScript para integração de aplicações web com o agente desktop **CyberCore Signer** via protocolo **CCSP/1**.
|
SDK oficial em JavaScript/TypeScript para integração de aplicações web com o agente desktop **CyberCore Signer** via protocolo **CCSP/1**.
|
||||||
|
|
||||||
Permite que sistemas web executem assinaturas digitais ICP-Brasil padrão **A1** (.pfx/.p12) e **A3** (Tokens USB e Smart Cards) instalados localmente na máquina do usuário, mantendo as chaves privadas e o PIN sob proteção estrita do hardware.
|
Permite que sistemas web solicitem assinaturas digitais ICP-Brasil padrão **A1** (.pfx/.p12) e **A3** (Tokens USB e Smart Cards via PKCS#11 / Windows Store) instalados localmente na máquina do usuário, mantendo as chaves privadas e o PIN sob proteção estrita e exclusiva do computador do cliente.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Instalação e Uso Rápido
|
## 📦 Instalação via Registry npm Privado
|
||||||
|
|
||||||
### 1. Inclusão Direta no HTML (ES Module)
|
Configure o escopo `@cybercore` no arquivo `.npmrc` da sua aplicação web:
|
||||||
```html
|
|
||||||
<script type="module">
|
|
||||||
import { CyberCoreSigner } from './src/signer-sdk.js';
|
|
||||||
|
|
||||||
const signer = new CyberCoreSigner({
|
```ini
|
||||||
applicationName: 'Portal Minha Empresa'
|
# .npmrc
|
||||||
});
|
@cybercore:registry=https://git.cybercore.com.br/api/packages/cybercore/npm/
|
||||||
|
//git.cybercore.com.br/api/packages/cybercore/npm/:_authToken=${GITEA_NPM_TOKEN}
|
||||||
|
always-auth=true
|
||||||
|
```
|
||||||
|
|
||||||
// 1. Verifica se o agente desktop está rodando
|
Em seguida, instale o pacote:
|
||||||
if (await signer.isInstalled()) {
|
```bash
|
||||||
console.log('Agente detectado!');
|
npm install @cybercore/signer-sdk
|
||||||
|
# ou
|
||||||
|
yarn add @cybercore/signer-sdk
|
||||||
|
# ou
|
||||||
|
pnpm add @cybercore/signer-sdk
|
||||||
|
```
|
||||||
|
|
||||||
// 2. Conecta e solicita autorização ao usuário
|
---
|
||||||
|
|
||||||
|
## 🚀 Uso Rápido (TypeScript / JavaScript ES Modules)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
CyberCoreSigner,
|
||||||
|
AgentNotRunningError,
|
||||||
|
UserCancelledError
|
||||||
|
} from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
const signer = new CyberCoreSigner({
|
||||||
|
applicationName: 'Portal Corporativo ERP',
|
||||||
|
timeoutMs: 30000 // 30 segundos
|
||||||
|
});
|
||||||
|
|
||||||
|
async function executarAssinatura() {
|
||||||
|
try {
|
||||||
|
// 1. Verifica se o agente desktop está ativo na bandeja do sistema
|
||||||
|
if (!(await signer.isInstalled())) {
|
||||||
|
alert('Por favor, abra o CyberCore Signer na bandeja do sistema.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Conecta e obtém autorização visual do usuário
|
||||||
await signer.connect();
|
await signer.connect();
|
||||||
|
|
||||||
// 3. Obtém certificados disponíveis
|
// 3. Obtém certificados disponíveis
|
||||||
const certs = await signer.getCertificates();
|
const certs = await signer.getCertificates();
|
||||||
console.log('Certificados encontrados:', certs);
|
if (certs.length === 0) {
|
||||||
|
alert('Nenhum certificado A1 ou token A3 conectado.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Assina um documento (o SDK calcula o SHA-256 no próprio navegador)
|
// 4. Assina o digest de um documento (o hash é gerado no próprio navegador)
|
||||||
const result = await signer.sign({
|
const resultado = await signer.sign({
|
||||||
certificateId: certs[0].id,
|
certificateId: certs[0].id,
|
||||||
data: 'Texto ou buffer do contrato para assinatura'
|
data: 'Conteúdo ou contrato em texto a ser assinado',
|
||||||
|
documentName: 'contrato.pdf'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Assinatura Base64:', result.signature);
|
console.log('Assinatura Base64:', resultado.signature);
|
||||||
|
console.log('Certificado X.509:', resultado.certificate);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AgentNotRunningError) {
|
||||||
|
alert(err.actionableGuidance);
|
||||||
|
} else if (err instanceof UserCancelledError) {
|
||||||
|
console.info('Assinatura cancelada pelo usuário.');
|
||||||
|
} else {
|
||||||
|
console.error('Falha na operação:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -45,20 +88,33 @@ Permite que sistemas web executem assinaturas digitais ICP-Brasil padrão **A1**
|
|||||||
|
|
||||||
| Método | Retorno | Descrição |
|
| Método | Retorno | Descrição |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `isInstalled(timeoutMs)` | `Promise<boolean>` | Verifica se o CyberCore Signer está ativo na porta local `21325`. |
|
| `isInstalled(timeoutMs?)` | `Promise<boolean>` | Verifica se o CyberCore Signer está ativo na porta local (`21325`). |
|
||||||
| `getStatus()` | `Promise<Object>` | Retorna status de prontidão, versão e sistema operacional. |
|
| `getStatus(timeoutMs?)` | `Promise<StatusResponse>` | Retorna status de prontidão, versão e sistema operacional. |
|
||||||
| `getVersion()` | `Promise<Object>` | Retorna versões do agente e do protocolo CCSP. |
|
| `getVersion()` | `Promise<VersionInfo>` | Retorna versões do agente e do protocolo CCSP. |
|
||||||
| `getCapabilities()` | `Promise<Object>` | Retorna algoritmos suportados (`SHA256withRSA`, etc.) e tipos de certificados. |
|
| `getCapabilities()` | `Promise<CapabilitiesResponse>` | Retorna algoritmos suportados (`SHA256withRSA`, etc.) e tipos de certificados. |
|
||||||
| `connect()` | `Promise<boolean>` | Realiza o handshake de autorização da aplicação web com o usuário. |
|
| `connect()` | `Promise<boolean>` | Realiza o handshake de autorização da aplicação web com o usuário. |
|
||||||
| `getCertificates()` | `Promise<Array>` | Retorna a lista de certificados A1 e A3 disponíveis na máquina. |
|
| `getCertificates()` | `Promise<CertificateDto[]>` | Retorna a lista de certificados A1 e A3 disponíveis na máquina. |
|
||||||
| `sign(options)` | `Promise<Object>` | Solicita a assinatura digital de um digest ou documento. |
|
| `sign(params)` | `Promise<SignResult>` | Assinatura desacoplada de digest SHA-256 (recomendado para alta performance). |
|
||||||
| `verify(options)` | `Promise<boolean>` | Validação criptográfica da integridade da assinatura. |
|
| `signPdfDocument(params)` | `Promise<SignPdfDocumentResult>` | Assinatura PAdES completa de PDF com estampa gráfica e posicionamento visual. |
|
||||||
|
| `signXmlDocument(params)` | `Promise<SignXmlDocumentResult>` | Assinatura XAdES-BES / XMLDSig Enveloped de arquivos XML ou notas fiscais. |
|
||||||
|
| `authenticate(params)` | `Promise<AuthenticateResult>` | Login seguro sem senha por desafio criptográfico (Challenge-Response). |
|
||||||
|
| `getSealThemes()` | `Promise<SealThemesResponse>` | Consulta temas de selos gráficos visuais disponíveis no agente. |
|
||||||
|
| `verify(params)` | `Promise<boolean>` | Validação sintática e integridade da assinatura no client. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔒 Princípios de Segurança e Privacidade
|
## 📂 Exemplos Prontos
|
||||||
|
|
||||||
- **Apenas o Digest é transmitido:** O documento completo nunca sai do navegador da aplicação web. O SDK calcula o hash SHA-256 localmente antes de invocar o agente.
|
Consulte a pasta [`examples/`](./examples/) para implementações completas:
|
||||||
- **Isolamento Total:** A comunicação ocorre exclusivamente via loopback em `127.0.0.1:21325`.
|
- [`01-crypto-login.js`](./examples/01-crypto-login.js) — Autenticação sem senha via desafio.
|
||||||
- **Autorização Prévia:** Aplicações web desconhecidas devem ser autorizadas visualmente pelo usuário na tela do CyberCore Signer antes de listar certificados.
|
- [`02-sign-digest.js`](./examples/02-sign-digest.js) — Assinatura desacoplada de digest SHA-256.
|
||||||
- **PIN Protegido:** O PIN do token A3 nunca trafega pela web e nunca é enviado a nenhum servidor remoto.
|
- [`03-sign-pades.js`](./examples/03-sign-pades.js) — Assinatura de PDF com carimbo visual ICP-Brasil / Gov.br.
|
||||||
|
- [`04-error-handling.js`](./examples/04-error-handling.js) — Tratamento completo e acionável de erros.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Segurança, Conectividade e Protocolo
|
||||||
|
|
||||||
|
- **Especificação do Protocolo:** Consulte a documentação formal em [`docs/CCSP.md`](../docs/CCSP.md) e o esquema JSON em [`docs/ccsp-v1.schema.json`](../docs/ccsp-v1.schema.json).
|
||||||
|
- **Compatibilidade HTTPS e Private Network Access (PNA):** Consulte [`docs/browser-compatibility.md`](../docs/browser-compatibility.md).
|
||||||
|
- **Processo de Release do Pacote:** Consulte [`docs/sdk-release-process.md`](../docs/sdk-release-process.md).
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Exemplo 01: Login Seguro via Certificado Digital (Challenge-Response)
|
||||||
|
*
|
||||||
|
* Demonstra como autenticar um usuário sem senhas, assinando um desafio emitido
|
||||||
|
* pelo servidor backend da sua aplicação web.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CyberCoreSigner, AgentNotRunningError } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const signer = new CyberCoreSigner({
|
||||||
|
applicationName: 'Portal Corporativo ERP'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Verifica se o agente desktop está ativo na máquina
|
||||||
|
const isRunning = await signer.isInstalled();
|
||||||
|
if (!isRunning) {
|
||||||
|
console.error('O CyberCore Signer não está em execução. Baixe e abra o aplicativo para continuar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Conecta e obtém autorização
|
||||||
|
console.log('Conectando ao agente...');
|
||||||
|
await signer.connect();
|
||||||
|
|
||||||
|
// 3. Lista os certificados digitais disponíveis
|
||||||
|
const certs = await signer.getCertificates();
|
||||||
|
console.log(`Foram localizados ${certs.length} certificado(s) no computador:`);
|
||||||
|
certs.forEach((c, idx) => {
|
||||||
|
console.log(`[${idx + 1}] ${c.alias} (${c.certificate_type}) - ${c.subject_name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (certs.length === 0) {
|
||||||
|
console.warn('Nenhum certificado digital A1 ou token A3 encontrado.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seleciona o primeiro certificado disponível
|
||||||
|
const selectedCert = certs[0];
|
||||||
|
|
||||||
|
// 4. Obtém o desafio emitido pelo seu backend (simulado aqui)
|
||||||
|
const serverChallenge = `auth-session-${Date.now()}-${Math.random().toString(36).substring(2)}`;
|
||||||
|
console.log(`Desafio recebido do servidor: ${serverChallenge}`);
|
||||||
|
|
||||||
|
// 5. Executa a autenticação criptográfica
|
||||||
|
console.log('Solicitando confirmação de login no CyberCore Signer...');
|
||||||
|
const authResult = await signer.authenticate({
|
||||||
|
certificateId: selectedCert.id,
|
||||||
|
challenge: serverChallenge
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Login realizado com sucesso!');
|
||||||
|
console.log('Titular:', authResult.subjectName);
|
||||||
|
console.log('CPF/CNPJ:', authResult.subjectCpfCnpj);
|
||||||
|
console.log('Algoritmo:', authResult.algorithm);
|
||||||
|
console.log('Assinatura do Desafio (Base64):', authResult.signature.substring(0, 32) + '...');
|
||||||
|
|
||||||
|
// O payload retornado deve ser enviado ao seu backend para verificação final:
|
||||||
|
// POST /api/auth/verify-signature { challenge, signature, certificate }
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
if (err instanceof AgentNotRunningError) {
|
||||||
|
console.error('Agente offline!\n' + err.actionableGuidance);
|
||||||
|
} else {
|
||||||
|
console.error(`Erro no login (${err.code}):`, err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Exemplo 02: Assinatura Desacoplada de Digest (Hash SHA-256)
|
||||||
|
*
|
||||||
|
* Demonstra a arquitetura recomendada do CCSP/1:
|
||||||
|
* O documento NUNCA sai do navegador / servidor da aplicação. Apenas o hash
|
||||||
|
* de 32 bytes (SHA-256) é enviado ao CyberCore Signer para ser assinado localmente.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const signer = new CyberCoreSigner({
|
||||||
|
applicationName: 'Sistema de Prontuário Eletrônico'
|
||||||
|
});
|
||||||
|
|
||||||
|
await signer.connect();
|
||||||
|
|
||||||
|
const certs = await signer.getCertificates();
|
||||||
|
if (certs.length === 0) {
|
||||||
|
throw new Error('Nenhum certificado disponível.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCert = certs[0];
|
||||||
|
|
||||||
|
// Simula o documento clínico ou contrato gerado na aplicação
|
||||||
|
const documentoTexto = 'Evolução clínica do paciente João da Silva - 20/10/2026. Procedimento sem intercorrências.';
|
||||||
|
|
||||||
|
// 1. Calcula o SHA-256 no próprio ambiente local
|
||||||
|
const digestBase64 = await signer._calculateSha256(documentoTexto);
|
||||||
|
console.log('Hash SHA-256 gerado localmente:', digestBase64);
|
||||||
|
|
||||||
|
// 2. Envia apenas o digest para o CyberCore Signer
|
||||||
|
console.log('Aguardando confirmação do usuário no diálogo nativo...');
|
||||||
|
const result = await signer.sign({
|
||||||
|
certificateId: selectedCert.id,
|
||||||
|
digest: digestBase64,
|
||||||
|
documentName: 'evolucao_clinica_1048.txt',
|
||||||
|
documentType: 'TXT',
|
||||||
|
purpose: 'Assinatura de Prontuário Médico',
|
||||||
|
expiresInSeconds: 120
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Documento assinado com sucesso!');
|
||||||
|
console.log('Status:', result.status);
|
||||||
|
console.log('Request ID:', result.requestId);
|
||||||
|
console.log('Algoritmo:', result.algorithm);
|
||||||
|
console.log('Assinatura (Base64):', result.signature.substring(0, 48) + '...');
|
||||||
|
console.log('Cadeia X.509 recebida:', result.certificateChain.length, 'certificado(s)');
|
||||||
|
|
||||||
|
// 3. Validação preliminar da assinatura no client
|
||||||
|
const isValid = await signer.verify({
|
||||||
|
signature: result.signature,
|
||||||
|
digest: result.digest,
|
||||||
|
certificate: result.certificate
|
||||||
|
});
|
||||||
|
console.log('Assinatura é consistente?', isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Exemplo 03: Assinatura de PDF Completo PAdES com Selo Visual
|
||||||
|
*
|
||||||
|
* Demonstra como assinar um arquivo PDF aplicando o selo gráfico ICP-Brasil ou Gov.br
|
||||||
|
* estilizado pelo CyberCore Signer, retornando o PDF assinado completo em Base64/Blob.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const signer = new CyberCoreSigner({
|
||||||
|
applicationName: 'Assinador de Contratos Online'
|
||||||
|
});
|
||||||
|
|
||||||
|
await signer.connect();
|
||||||
|
|
||||||
|
// 1. Consulta os temas de selos visuais disponíveis no agente
|
||||||
|
const themesResp = await signer.getSealThemes();
|
||||||
|
console.log('Selo padrão configurado:', themesResp.default_seal_id);
|
||||||
|
console.log('Temas disponíveis:', themesResp.seals.map(s => s.name).join(', '));
|
||||||
|
|
||||||
|
// 2. Mock de um PDF de teste (no navegador, normalmente é obtido via <input type="file">)
|
||||||
|
const fakePdfContent = Buffer.from('%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n%%EOF');
|
||||||
|
|
||||||
|
const certs = await signer.getCertificates();
|
||||||
|
if (certs.length === 0) {
|
||||||
|
console.error('Nenhum certificado para assinatura.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Executa a assinatura com estampa gráfica posicionada no rodapé da última página
|
||||||
|
console.log('Enviando solicitação de assinatura PAdES...');
|
||||||
|
const result = await signer.signPdfDocument({
|
||||||
|
pdf: fakePdfContent,
|
||||||
|
certificateId: certs[0].id,
|
||||||
|
documentName: 'contrato_prestacao_servicos.pdf',
|
||||||
|
position: 'BOTTOM_RIGHT',
|
||||||
|
pageChoice: 'LAST_PAGE',
|
||||||
|
scale: 1.0,
|
||||||
|
visibleSeal: true,
|
||||||
|
sealThemeId: 'icp-brasil',
|
||||||
|
reason: 'Concordância integral com os termos do contrato',
|
||||||
|
location: 'Brasília - DF',
|
||||||
|
cpfDisplayMode: 'MASKED', // Exibe CPF mascarado no carimbo (ex: ***.456.789-**)
|
||||||
|
includeDateTime: true,
|
||||||
|
customExtraInfo: 'Matrícula: 55420-9'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('PDF assinado com sucesso!');
|
||||||
|
console.log('Titular:', result.subjectName);
|
||||||
|
console.log('CPF/CNPJ:', result.subjectCpfCnpj);
|
||||||
|
console.log('Tamanho do PDF assinado:', result.signedPdf.length, 'caracteres Base64');
|
||||||
|
|
||||||
|
// No navegador web, você pode invocar diretamente:
|
||||||
|
// result.download('contrato_assinado.pdf');
|
||||||
|
// Ou obter os bytes binários para envio via fetch/multipart:
|
||||||
|
const pdfBytes = result.getSignedPdfBytes();
|
||||||
|
console.log('Bytes binários gerados:', pdfBytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Exemplo 04: Tratamento Estruturado e Acionável de Erros
|
||||||
|
*
|
||||||
|
* Demonstra como capturar cada erro específico do CCSP/1 e orientar
|
||||||
|
* o usuário final com mensagens claras e passos práticos de resolução.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CyberCoreSigner,
|
||||||
|
AgentNotRunningError,
|
||||||
|
ApplicationNotAuthorizedError,
|
||||||
|
UserCancelledError,
|
||||||
|
CertificateNotFoundError,
|
||||||
|
CertificateExpiredError,
|
||||||
|
CertificateInvalidError,
|
||||||
|
PrivateKeyNotAvailableError,
|
||||||
|
PinRequiredError,
|
||||||
|
PinIncorrectError,
|
||||||
|
TokenNotFoundError,
|
||||||
|
RequestExpiredError,
|
||||||
|
TimeoutError,
|
||||||
|
CyberCoreError
|
||||||
|
} from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
async function performSigningOperation(signer, certId, fileDigest) {
|
||||||
|
try {
|
||||||
|
return await signer.sign({
|
||||||
|
certificateId: certId,
|
||||||
|
digest: fileDigest,
|
||||||
|
documentName: 'termo_responsabilidade.pdf'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AgentNotRunningError) {
|
||||||
|
// O agente local está fechado ou bloqueado
|
||||||
|
console.warn('⚠️ AGENTE NÃO DETECTADO:');
|
||||||
|
console.warn(err.actionableGuidance);
|
||||||
|
// Sugestão de UI: exibir banner com botão "Baixar CyberCore Signer"
|
||||||
|
} else if (err instanceof ApplicationNotAuthorizedError) {
|
||||||
|
// O usuário clicou em "Rejeitar" no prompt de autorização da aplicação
|
||||||
|
console.warn('⚠️ APLICAÇÃO NÃO AUTORIZADA:');
|
||||||
|
console.warn('O usuário não autorizou este portal no diálogo de segurança do CyberCore Signer.');
|
||||||
|
} else if (err instanceof UserCancelledError) {
|
||||||
|
// O usuário cancelou a assinatura explicitamente na janela nativa
|
||||||
|
console.info('ℹ️ Assinatura cancelada pelo usuário.');
|
||||||
|
} else if (err instanceof CertificateExpiredError) {
|
||||||
|
console.error('❌ CERTIFICADO EXPIRADO: O certificado digital selecionado expirou.');
|
||||||
|
} else if (err instanceof CertificateInvalidError) {
|
||||||
|
console.error('❌ CERTIFICADO INVÁLIDO: O certificado possui estrutura corrompida ou foi revogado.');
|
||||||
|
} else if (err instanceof PrivateKeyNotAvailableError) {
|
||||||
|
console.error('❌ CHAVE PRIVADA INDISPONÍVEL: A chave privada deste certificado não está acessível no computador.');
|
||||||
|
} else if (err instanceof PinIncorrectError) {
|
||||||
|
console.error('❌ PIN INCORRETO: O PIN digitado para o token A3 está errado. Cuidado com o bloqueio do dispositivo.');
|
||||||
|
} else if (err instanceof PinRequiredError) {
|
||||||
|
console.error('❌ PIN OBRIGATÓRIO: É necessário fornecer o PIN do dispositivo criptográfico.');
|
||||||
|
} else if (err instanceof TokenNotFoundError) {
|
||||||
|
console.error('❌ TOKEN NÃO ENCONTRADO: O token USB ou smart card foi removido da porta USB.');
|
||||||
|
} else if (err instanceof RequestExpiredError) {
|
||||||
|
console.warn('⚠️ REQUISIÇÃO EXPIRADA: O tempo limite para confirmação do usuário esgotou.');
|
||||||
|
} else if (err instanceof TimeoutError) {
|
||||||
|
console.error('⏱️ TIMEOUT: O agente demorou mais tempo que o esperado para responder.');
|
||||||
|
} else if (err instanceof CyberCoreError) {
|
||||||
|
console.error(`⚠️ ERRO DO PROTOCOLO CCSP (${err.code}):`, err.message);
|
||||||
|
} else {
|
||||||
|
console.error('Erro desconhecido:', err);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const signer = new CyberCoreSigner({ timeoutMs: 15000 });
|
||||||
|
const fakeDigest = Buffer.alloc(32, 9).toString('base64');
|
||||||
|
|
||||||
|
console.log('Testando tratamento de erros com certificado simulado expirado...');
|
||||||
|
try {
|
||||||
|
await performSigningOperation(signer, 'cert-expirado', fakeDigest);
|
||||||
|
} catch {
|
||||||
|
console.log('Tratamento de exceções concluído com sucesso.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(() => {});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Exemplos de Integração — CyberCore Signer Web SDK
|
||||||
|
|
||||||
|
Este diretório contém exemplos práticos e prontos para uso demonstrando os principais fluxos de integração do `@cybercore/signer-sdk` com aplicações web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Arquivos de Exemplo
|
||||||
|
|
||||||
|
| Arquivo | Descrição |
|
||||||
|
| :--- | :--- |
|
||||||
|
| [`01-crypto-login.js`](./01-crypto-login.js) | Autenticação segura por desafio criptográfico (Challenge-Response) sem senhas. |
|
||||||
|
| [`02-sign-digest.js`](./02-sign-digest.js) | Assinatura desacoplada em que o navegador calcula o hash SHA-256 e o CyberCore assina apenas o digest. |
|
||||||
|
| [`03-sign-pades.js`](./03-sign-pades.js) | Assinatura digital completa de PDF (PAdES) com selo gráfico posicionado (ICP-Brasil / Gov.br). |
|
||||||
|
| [`04-error-handling.js`](./04-error-handling.js) | Captura e tratamento estruturado de todos os erros do protocolo CCSP/1 com guia acionável. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Como Executar
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
1. Agente desktop **CyberCore Signer** em execução na máquina (`127.0.0.1:21325`).
|
||||||
|
2. Node.js 18+ instalado.
|
||||||
|
|
||||||
|
### Execução direta via Node.js
|
||||||
|
Como o pacote usa módulos ES (`"type": "module"`), você pode testar diretamente com:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Executar exemplo de login
|
||||||
|
node examples/01-crypto-login.js
|
||||||
|
|
||||||
|
# Executar exemplo de assinatura de digest
|
||||||
|
node examples/02-sign-digest.js
|
||||||
|
|
||||||
|
# Executar exemplo de assinatura PAdES com selo
|
||||||
|
node examples/03-sign-pades.js
|
||||||
|
|
||||||
|
# Executar exemplo de tratamento de erros
|
||||||
|
node examples/04-error-handling.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Uso em Projetos Frontend (React, Vue, Angular, Svelte, Vanilla)
|
||||||
|
|
||||||
|
Instale o pacote a partir do seu registry npm privado:
|
||||||
|
```bash
|
||||||
|
npm install @cybercore/signer-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
Importe os métodos e tipos em qualquer componente ou serviço web:
|
||||||
|
```typescript
|
||||||
|
import { CyberCoreSigner, AgentNotRunningError } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
const signer = new CyberCoreSigner({
|
||||||
|
applicationName: 'Meu Sistema Web'
|
||||||
|
});
|
||||||
|
|
||||||
|
async function assinarDocumento(arquivoPdf: File, certId: string) {
|
||||||
|
try {
|
||||||
|
const resultado = await signer.signPdfDocument({
|
||||||
|
pdf: arquivoPdf,
|
||||||
|
certificateId: certId,
|
||||||
|
position: 'BOTTOM_RIGHT',
|
||||||
|
sealThemeId: 'icp-brasil'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Baixa o documento assinado no navegador
|
||||||
|
resultado.download('documento_assinado.pdf');
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AgentNotRunningError) {
|
||||||
|
alert(err.actionableGuidance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,19 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "@cybercore/signer-sdk",
|
"name": "@cybercore/signer-sdk",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "SDK JavaScript para integração de aplicações web com o agente desktop CyberCore Signer via protocolo CCSP/1",
|
"description": "SDK JavaScript/TypeScript para integração de aplicações web com o agente desktop CyberCore Signer via protocolo CCSP/1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/signer-sdk.js",
|
"main": "src/signer-sdk.js",
|
||||||
"module": "src/signer-sdk.js",
|
"module": "src/signer-sdk.js",
|
||||||
"scripts": {
|
"types": "src/signer-sdk.d.ts",
|
||||||
"test": "node --test test/signer-sdk.test.js"
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/signer-sdk.d.ts",
|
||||||
|
"import": "./src/signer-sdk.js",
|
||||||
|
"default": "./src/signer-sdk.js"
|
||||||
|
},
|
||||||
|
"./errors": {
|
||||||
|
"types": "./src/errors.d.ts",
|
||||||
|
"import": "./src/errors.js",
|
||||||
|
"default": "./src/errors.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"author": "CyberCore",
|
"files": [
|
||||||
|
"src",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "restricted"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test test/*.test.js"
|
||||||
|
},
|
||||||
|
"author": "CyberCore Systems",
|
||||||
"license": "Proprietary",
|
"license": "Proprietary",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cybercore",
|
"cybercore",
|
||||||
"digital-signature",
|
"digital-signature",
|
||||||
"icp-brasil",
|
"icp-brasil",
|
||||||
|
"pades",
|
||||||
|
"xmldsig",
|
||||||
"a1",
|
"a1",
|
||||||
"a3",
|
"a3",
|
||||||
"token",
|
"token",
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Definições TypeScript para os erros estruturados do protocolo CCSP/1 (@cybercore/signer-sdk).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CcspErrorCode =
|
||||||
|
| 'AGENT_NOT_RUNNING'
|
||||||
|
| 'COMMUNICATION_ERROR'
|
||||||
|
| 'APPLICATION_NOT_AUTHORIZED'
|
||||||
|
| 'USER_CANCELLED'
|
||||||
|
| 'CERTIFICATE_NOT_FOUND'
|
||||||
|
| 'CERTIFICATE_EXPIRED'
|
||||||
|
| 'CERTIFICATE_INVALID'
|
||||||
|
| 'PRIVATE_KEY_NOT_AVAILABLE'
|
||||||
|
| 'PIN_REQUIRED'
|
||||||
|
| 'PIN_INCORRECT'
|
||||||
|
| 'TOKEN_NOT_FOUND'
|
||||||
|
| 'TOKEN_REMOVED'
|
||||||
|
| 'PKCS11_ERROR'
|
||||||
|
| 'REQUEST_EXPIRED'
|
||||||
|
| 'REPLAY_DETECTED'
|
||||||
|
| 'INVALID_ORIGIN'
|
||||||
|
| 'UNSUPPORTED_ALGORITHM'
|
||||||
|
| 'PROVIDER_NOT_AVAILABLE'
|
||||||
|
| 'SIGNATURE_FAILED'
|
||||||
|
| 'TIMEOUT_ERROR'
|
||||||
|
| 'BAD_REQUEST'
|
||||||
|
| 'SIGN_ERROR'
|
||||||
|
| 'SIGN_PDF_ERROR'
|
||||||
|
| 'SIGN_XML_ERROR'
|
||||||
|
| 'AUTH_ERROR'
|
||||||
|
| 'CONNECTION_FAILED'
|
||||||
|
| string;
|
||||||
|
|
||||||
|
export class CyberCoreError extends Error {
|
||||||
|
code: CcspErrorCode;
|
||||||
|
originalError?: unknown;
|
||||||
|
constructor(message: string, code?: CcspErrorCode, originalError?: unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentNotRunningOptions {
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AgentNotRunningError extends CyberCoreError {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
actionableGuidance: string;
|
||||||
|
constructor(message?: string, options?: AgentNotRunningOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApplicationNotAuthorizedError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserCancelledError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateNotFoundError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateExpiredError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateInvalidError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivateKeyNotAvailableError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PinRequiredError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PinIncorrectError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokenNotFoundError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestExpiredError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReplayDetectedError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidOriginError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnsupportedAlgorithmError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProviderNotAvailableError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SignatureFailedError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeoutError extends CyberCoreError {
|
||||||
|
constructor(message?: string);
|
||||||
|
}
|
||||||
@@ -12,9 +12,19 @@ export class CyberCoreError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AgentNotRunningError extends CyberCoreError {
|
export class AgentNotRunningError extends CyberCoreError {
|
||||||
constructor(message = 'O agente desktop CyberCore Signer não está em execução ou não foi detectado em 127.0.0.1:21325.') {
|
constructor(
|
||||||
|
message = 'O agente desktop CyberCore Signer não está em execução ou não foi detectado em 127.0.0.1:21325. Certifique-se de que o aplicativo está instalado e aberto na bandeja do sistema (system tray).',
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
super(message, 'AGENT_NOT_RUNNING');
|
super(message, 'AGENT_NOT_RUNNING');
|
||||||
this.name = 'AgentNotRunningError';
|
this.name = 'AgentNotRunningError';
|
||||||
|
this.host = options.host || '127.0.0.1';
|
||||||
|
this.port = options.port || 21325;
|
||||||
|
this.actionableGuidance = [
|
||||||
|
'1. Verifique se o ícone do CyberCore Signer está ativo na barra de tarefas / bandeja do sistema.',
|
||||||
|
'2. Se o aplicativo ainda não estiver instalado, baixe e execute o instalador oficial do CyberCore Signer.',
|
||||||
|
'3. Certifique-se de que a porta local 21325 não está bloqueada por firewall ou softwares antivírus.'
|
||||||
|
].join('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,21 +36,42 @@ export class ApplicationNotAuthorizedError extends CyberCoreError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UserCancelledError extends CyberCoreError {
|
export class UserCancelledError extends CyberCoreError {
|
||||||
constructor(message = 'A operação de assinatura foi cancelada pelo usuário.') {
|
constructor(message = 'A operação de assinatura foi cancelada pelo usuário no diálogo nativo.') {
|
||||||
super(message, 'USER_CANCELLED');
|
super(message, 'USER_CANCELLED');
|
||||||
this.name = 'UserCancelledError';
|
this.name = 'UserCancelledError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CertificateNotFoundError extends CyberCoreError {
|
export class CertificateNotFoundError extends CyberCoreError {
|
||||||
constructor(message = 'Certificado digital não encontrado ou não selecionado.') {
|
constructor(message = 'Certificado digital não encontrado ou não selecionado no agente.') {
|
||||||
super(message, 'CERTIFICATE_NOT_FOUND');
|
super(message, 'CERTIFICATE_NOT_FOUND');
|
||||||
this.name = 'CertificateNotFoundError';
|
this.name = 'CertificateNotFoundError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CertificateExpiredError extends CyberCoreError {
|
||||||
|
constructor(message = 'O certificado digital selecionado expirou e não pode ser utilizado para assinatura.') {
|
||||||
|
super(message, 'CERTIFICATE_EXPIRED');
|
||||||
|
this.name = 'CertificateExpiredError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateInvalidError extends CyberCoreError {
|
||||||
|
constructor(message = 'O certificado digital é inválido, foi revogado ou possui estrutura X.509 corrompida.') {
|
||||||
|
super(message, 'CERTIFICATE_INVALID');
|
||||||
|
this.name = 'CertificateInvalidError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivateKeyNotAvailableError extends CyberCoreError {
|
||||||
|
constructor(message = 'A chave privada do certificado não está acessível no computador ou o dispositivo físico está ausente.') {
|
||||||
|
super(message, 'PRIVATE_KEY_NOT_AVAILABLE');
|
||||||
|
this.name = 'PrivateKeyNotAvailableError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PinRequiredError extends CyberCoreError {
|
export class PinRequiredError extends CyberCoreError {
|
||||||
constructor(message = 'PIN do token ou smart card A3 é obrigatório.') {
|
constructor(message = 'O PIN do token USB ou smart card A3 é obrigatório para autorizar a chave privada.') {
|
||||||
super(message, 'PIN_REQUIRED');
|
super(message, 'PIN_REQUIRED');
|
||||||
this.name = 'PinRequiredError';
|
this.name = 'PinRequiredError';
|
||||||
}
|
}
|
||||||
@@ -54,14 +85,14 @@ export class PinIncorrectError extends CyberCoreError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TokenNotFoundError extends CyberCoreError {
|
export class TokenNotFoundError extends CyberCoreError {
|
||||||
constructor(message = 'Token USB ou smart card não foi detectado no computador.') {
|
constructor(message = 'Token USB ou smart card não foi detectado no computador ou foi removido.') {
|
||||||
super(message, 'TOKEN_NOT_FOUND');
|
super(message, 'TOKEN_NOT_FOUND');
|
||||||
this.name = 'TokenNotFoundError';
|
this.name = 'TokenNotFoundError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequestExpiredError extends CyberCoreError {
|
export class RequestExpiredError extends CyberCoreError {
|
||||||
constructor(message = 'A solicitação de assinatura expirou.') {
|
constructor(message = 'A solicitação de assinatura expirou antes de ser processada.') {
|
||||||
super(message, 'REQUEST_EXPIRED');
|
super(message, 'REQUEST_EXPIRED');
|
||||||
this.name = 'RequestExpiredError';
|
this.name = 'RequestExpiredError';
|
||||||
}
|
}
|
||||||
@@ -73,3 +104,38 @@ export class ReplayDetectedError extends CyberCoreError {
|
|||||||
this.name = 'ReplayDetectedError';
|
this.name = 'ReplayDetectedError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InvalidOriginError extends CyberCoreError {
|
||||||
|
constructor(message = 'A origem web solicitante não confere com os domínios homologados para esta aplicação.') {
|
||||||
|
super(message, 'INVALID_ORIGIN');
|
||||||
|
this.name = 'InvalidOriginError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnsupportedAlgorithmError extends CyberCoreError {
|
||||||
|
constructor(message = 'O algoritmo criptográfico solicitado não é suportado pelo provedor de certificados.') {
|
||||||
|
super(message, 'UNSUPPORTED_ALGORITHM');
|
||||||
|
this.name = 'UnsupportedAlgorithmError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProviderNotAvailableError extends CyberCoreError {
|
||||||
|
constructor(message = 'O provedor criptográfico necessário não está disponível no sistema operacional.') {
|
||||||
|
super(message, 'PROVIDER_NOT_AVAILABLE');
|
||||||
|
this.name = 'ProviderNotAvailableError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SignatureFailedError extends CyberCoreError {
|
||||||
|
constructor(message = 'Falha durante o cálculo da assinatura digital.') {
|
||||||
|
super(message, 'SIGNATURE_FAILED');
|
||||||
|
this.name = 'SignatureFailedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeoutError extends CyberCoreError {
|
||||||
|
constructor(message = 'Tempo limite esgotado na comunicação com o agente local.') {
|
||||||
|
super(message, 'TIMEOUT_ERROR');
|
||||||
|
this.name = 'TimeoutError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './signer-sdk.js';
|
||||||
|
export { default } from './signer-sdk.js';
|
||||||
@@ -4,6 +4,25 @@
|
|||||||
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
|
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function escapeSvgText(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLogoUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
const trimmed = url.trim();
|
||||||
|
if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export class PdfSealDesigner {
|
export class PdfSealDesigner {
|
||||||
/**
|
/**
|
||||||
* @param {Object} options Configurações iniciais do designer.
|
* @param {Object} options Configurações iniciais do designer.
|
||||||
@@ -592,8 +611,9 @@ export class PdfSealDesigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let iconGraphic = '';
|
let iconGraphic = '';
|
||||||
if (t.customLogoBase64) {
|
const safeLogo = sanitizeLogoUrl(t.customLogoBase64);
|
||||||
iconGraphic = `<image xlink:href="${t.customLogoBase64}" href="${t.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
if (safeLogo) {
|
||||||
|
iconGraphic = `<image xlink:href="${safeLogo}" href="${safeLogo}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
} else {
|
} else {
|
||||||
switch (t.iconStyle) {
|
switch (t.iconStyle) {
|
||||||
case 'star':
|
case 'star':
|
||||||
@@ -640,11 +660,11 @@ export class PdfSealDesigner {
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Tipografia do Selo -->
|
<!-- Tipografia do Selo -->
|
||||||
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${escapeSvgText(t.primaryColor)}" letter-spacing="0.3">${escapeSvgText(t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE')}</text>
|
||||||
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${escapeSvgText(t.textColor)}">MARIA DA SILVA</text>
|
||||||
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${escapeSvgText([docText, extraText].filter(Boolean).join(' • '))}</text>
|
||||||
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${dateText ? `Data: ${dateText}` : ''}</text>
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${escapeSvgText(dateText ? `Data: ${dateText}` : '')}</text>
|
||||||
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${escapeSvgText(t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001')}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,25 @@
|
|||||||
* e geração do payload de assinatura em tempo real.
|
* e geração do payload de assinatura em tempo real.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function escapeSvgText(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLogoUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
const trimmed = url.trim();
|
||||||
|
if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export class PdfStampPicker {
|
export class PdfStampPicker {
|
||||||
/**
|
/**
|
||||||
* @param {Object} options Configurações do componente.
|
* @param {Object} options Configurações do componente.
|
||||||
@@ -949,8 +968,9 @@ export class PdfStampPicker {
|
|||||||
else if (name.length > 26) nameFontSize = 10.0;
|
else if (name.length > 26) nameFontSize = 10.0;
|
||||||
|
|
||||||
let iconGraphic = '';
|
let iconGraphic = '';
|
||||||
if (theme.customLogoBase64) {
|
const safeLogo = sanitizeLogoUrl(theme.customLogoBase64);
|
||||||
iconGraphic = `<image xlink:href="${theme.customLogoBase64}" href="${theme.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
if (safeLogo) {
|
||||||
|
iconGraphic = `<image xlink:href="${safeLogo}" href="${safeLogo}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
} else {
|
} else {
|
||||||
switch (theme.iconStyle || theme.id) {
|
switch (theme.iconStyle || theme.id) {
|
||||||
case 'star':
|
case 'star':
|
||||||
@@ -1004,11 +1024,11 @@ export class PdfStampPicker {
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Tipografia do Selo -->
|
<!-- Tipografia do Selo -->
|
||||||
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${theme.primaryColor || '#0d47a1'}" letter-spacing="0.3">${header}</text>
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${escapeSvgText(theme.primaryColor || '#0d47a1')}" letter-spacing="0.3">${escapeSvgText(header)}</text>
|
||||||
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="${nameFontSize}" font-weight="bold" fill="${theme.textColor || '#0f172a'}">${name}</text>
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="${nameFontSize}" font-weight="bold" fill="${escapeSvgText(theme.textColor || '#0f172a')}">${escapeSvgText(name)}</text>
|
||||||
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${doc}</text>
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${escapeSvgText(doc)}</text>
|
||||||
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">Data: ${dateText}</text>
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">Data: ${escapeSvgText(dateText)}</text>
|
||||||
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${footer}</text>
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${escapeSvgText(footer)}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||