ci(security): generate cyclonedx sbom, add sbom upload to ci and update stride security model

This commit is contained in:
Claudecio Martins
2026-10-20 15:13:52 -03:00
parent 300944697f
commit 06c90b4e86
7 changed files with 109 additions and 58 deletions
+10
View File
@@ -55,6 +55,16 @@ jobs:
modules/*/target/site/spotbugs/** modules/*/target/site/spotbugs/**
retention-days: 14 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: build-windows:
name: Build & Verify (Windows) name: Build & Verify (Windows)
# Runner Windows: requer act_runner nativo em Windows na instância Gitea. # Runner Windows: requer act_runner nativo em Windows na instância Gitea.
+46 -44
View File
@@ -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.
@@ -41,7 +41,7 @@ public class CyberCoreSignerApplication {
buildFlavor = flavor.trim().toLowerCase(); buildFlavor = flavor.trim().toLowerCase();
} }
} }
} catch (Exception ignored) {} } catch (java.io.IOException ignored) {}
} }
public static String getBuildFlavor() { public static String getBuildFlavor() {
@@ -189,7 +189,8 @@ public class CyberCoreSignerApplication {
try { try {
configuredPort = Integer.parseInt(args[++i].trim()); configuredPort = Integer.parseInt(args[++i].trim());
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
logger.warn("Porta inválida especificada em --port: '{}'. Usando porta padrão {}.", args[i], CcspServer.DEFAULT_PORT); String sanitized = args[i].replaceAll("[\r\n]", "");
logger.warn("Porta inválida especificada em --port: '{}'. Usando porta padrão {}.", sanitized, CcspServer.DEFAULT_PORT);
} }
} }
} }
@@ -54,10 +54,11 @@ public final class ApplicationRegistry {
} }
private static Path resolveKeyPathForStorage(Path storagePath) { private static Path resolveKeyPathForStorage(Path storagePath) {
if (storagePath == null || storagePath.getParent() == null) { if (storagePath == null) {
return null; return null;
} }
return storagePath.getParent().resolve(".vault_key"); Path parent = storagePath.getParent();
return parent != null ? parent.resolve(".vault_key") : null;
} }
private static Path resolveDefaultStoragePath() { private static Path resolveDefaultStoragePath() {
@@ -26,6 +26,7 @@ import java.util.Set;
public class HmacIntegrityManager { public class HmacIntegrityManager {
private static final Logger logger = LoggerFactory.getLogger(HmacIntegrityManager.class); 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 String HMAC_ALGORITHM = "HmacSHA256";
private static final int KEY_SIZE_BYTES = 32; private static final int KEY_SIZE_BYTES = 32;
private static final String DEFAULT_KEY_FILE = ".vault_key"; private static final String DEFAULT_KEY_FILE = ".vault_key";
@@ -50,7 +51,7 @@ public class HmacIntegrityManager {
private synchronized void initSecretKey() { private synchronized void initSecretKey() {
if (keyPath == null) { if (keyPath == null) {
byte[] ephemeralKey = new byte[KEY_SIZE_BYTES]; byte[] ephemeralKey = new byte[KEY_SIZE_BYTES];
new SecureRandom().nextBytes(ephemeralKey); SECURE_RANDOM.nextBytes(ephemeralKey);
this.secretKey = ephemeralKey; this.secretKey = ephemeralKey;
return; return;
} }
@@ -67,7 +68,7 @@ public class HmacIntegrityManager {
// Gera nova chave criptográfica forte // Gera nova chave criptográfica forte
byte[] newKey = new byte[KEY_SIZE_BYTES]; byte[] newKey = new byte[KEY_SIZE_BYTES];
new SecureRandom().nextBytes(newKey); SECURE_RANDOM.nextBytes(newKey);
this.secretKey = newKey; this.secretKey = newKey;
Path parent = keyPath.getParent(); Path parent = keyPath.getParent();
@@ -82,7 +83,7 @@ public class HmacIntegrityManager {
} catch (Exception e) { } catch (Exception e) {
logger.warn("Falha ao persistir chave em {}. Usando chave em memória: {}", keyPath, e.getMessage()); logger.warn("Falha ao persistir chave em {}. Usando chave em memória: {}", keyPath, e.getMessage());
byte[] fallbackKey = new byte[KEY_SIZE_BYTES]; byte[] fallbackKey = new byte[KEY_SIZE_BYTES];
new SecureRandom().nextBytes(fallbackKey); SECURE_RANDOM.nextBytes(fallbackKey);
this.secretKey = fallbackKey; this.secretKey = fallbackKey;
} }
} }
@@ -129,11 +130,14 @@ public class HmacIntegrityManager {
File file = path.toFile(); File file = path.toFile();
// Proteção padrão Java SE (funciona em Windows e Unix) // Proteção padrão Java SE (funciona em Windows e Unix)
file.setReadable(false, false); boolean ok = file.setReadable(false, false);
file.setReadable(true, true); ok = file.setReadable(true, true) && ok;
file.setWritable(false, false); ok = file.setWritable(false, false) && ok;
file.setWritable(true, true); ok = file.setWritable(true, true) && ok;
file.setExecutable(false, false); 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) // Permissões POSIX estritas (Linux / macOS)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
@@ -476,11 +476,15 @@ public class CyberCoreDesktopApplication extends Application {
if (docSize == null && request.pdf() != null) { if (docSize == null && request.pdf() != null) {
try { try {
docSize = (long) Base64.getDecoder().decode(request.pdf()).length; docSize = (long) Base64.getDecoder().decode(request.pdf()).length;
} catch (Exception ignored) {} } catch (IllegalArgumentException e) {
logger.debug("Não foi possível decodificar base64 do PDF: {}", e.getMessage());
}
} else if (docSize == null && request.xmlBase64() != null) { } else if (docSize == null && request.xmlBase64() != null) {
try { try {
docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length; docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length;
} catch (Exception ignored) {} } catch (IllegalArgumentException e) {
logger.debug("Não foi possível decodificar base64 do XML: {}", e.getMessage());
}
} }
String requestOrigin = request.origin(); String requestOrigin = request.origin();
+29
View File
@@ -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>