fix: enhance security with strict file permissions, pin isolation, and robust protocol validations
This commit is contained in:
+82
-39
@@ -218,16 +218,20 @@ public class CcspServer {
|
||||
appId = getQueryParam(uri, "application_id");
|
||||
}
|
||||
|
||||
// Verifica autorização se app_id e origin forem informados
|
||||
if (appId != null && !appId.isBlank()) {
|
||||
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
||||
if (!authorized) {
|
||||
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
|
||||
if (!approved) {
|
||||
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||
"Aplicação não autorizada pelo usuário.");
|
||||
return;
|
||||
}
|
||||
// Exige obrigatoriamente a identificação da aplicação para listagem de certificados
|
||||
if (appId == null || appId.isBlank()) {
|
||||
sendErrorResponse(exchange, 401, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||
"application_id (via query param ou cabeçalho X-CyberCore-AppId) é obrigatório.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
||||
if (!authorized) {
|
||||
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
|
||||
if (!approved) {
|
||||
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||
"Aplicação não autorizada pelo usuário.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,14 +354,17 @@ public class CcspServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Validação anti-replay (Nonce)
|
||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
// 3. Validação anti-replay (Nonce obrigatório)
|
||||
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||
return;
|
||||
}
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Validação de autorização da aplicação
|
||||
@@ -520,13 +527,17 @@ public class CcspServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
// Validação anti-replay (Nonce obrigatório)
|
||||
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||
return;
|
||||
}
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
|
||||
String appId = request.applicationId();
|
||||
@@ -735,13 +746,17 @@ public class CcspServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
// Validação anti-replay (Nonce obrigatório)
|
||||
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||
return;
|
||||
}
|
||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||
if (!accepted) {
|
||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||
"Requisição duplicada detectada (replay prevenido).");
|
||||
return;
|
||||
}
|
||||
|
||||
String appId = request.applicationId();
|
||||
@@ -1032,25 +1047,53 @@ public class CcspServer {
|
||||
}
|
||||
|
||||
private boolean handleCors(HttpExchange exchange) throws IOException {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
String origin = getOrigin(exchange);
|
||||
|
||||
if (origin == null || origin.isBlank()) {
|
||||
origin = "*";
|
||||
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
exchange.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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");
|
||||
// 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())) {
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
if (isHandshakePath || isAuthorizedOrigin) {
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
} else {
|
||||
exchange.sendResponseHeaders(403, -1);
|
||||
}
|
||||
exchange.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isLocalOrigin(String origin) {
|
||||
if (origin == null || origin.isBlank()) return false;
|
||||
try {
|
||||
URI uri = URI.create(origin);
|
||||
String host = uri.getHost();
|
||||
return "localhost".equalsIgnoreCase(host) || "127.0.0.1".equals(host);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getOrigin(HttpExchange exchange) {
|
||||
String origin = exchange.getRequestHeaders().getFirst("Origin");
|
||||
if (origin == null || origin.isBlank()) {
|
||||
|
||||
+76
-26
@@ -3,6 +3,8 @@ package br.com.cybercore.signer.core.security;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -12,15 +14,19 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer.
|
||||
* Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3
|
||||
* exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão".
|
||||
* Os PINs são escopados por aplicação web (applicationId + certId) e possuem expiração por inatividade (TTL de 15 min).
|
||||
* Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte.
|
||||
*/
|
||||
public class PinSessionManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class);
|
||||
private static final long SESSION_TTL_MINUTES = 15;
|
||||
|
||||
private static final PinSessionManager INSTANCE = new PinSessionManager();
|
||||
|
||||
private final Map<String, char[]> sessionPins = new ConcurrentHashMap<>();
|
||||
private record PinEntry(char[] pin, Instant lastAccessedAt) {}
|
||||
|
||||
private final Map<String, PinEntry> sessionPins = new ConcurrentHashMap<>();
|
||||
private volatile boolean alwaysPromptPin = false;
|
||||
|
||||
public PinSessionManager() {
|
||||
@@ -30,63 +36,107 @@ public class PinSessionManager {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private String buildKey(String applicationId, String certId) {
|
||||
String appId = (applicationId != null && !applicationId.isBlank()) ? applicationId.trim() : "default";
|
||||
return appId + "::" + certId.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Salva o PIN de um certificado/token para a sessão atual em memória volátil.
|
||||
* Salva o PIN associado a uma aplicação específica e certificado.
|
||||
*/
|
||||
public synchronized void savePin(String certId, char[] pin) {
|
||||
public synchronized void savePin(String applicationId, String certId, char[] pin) {
|
||||
Objects.requireNonNull(certId, "certId must not be null");
|
||||
String key = buildKey(applicationId, certId);
|
||||
if (pin == null || pin.length == 0) {
|
||||
clearPin(certId);
|
||||
clearPin(applicationId, certId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Limpa PIN anterior caso exista
|
||||
clearPin(certId);
|
||||
|
||||
sessionPins.put(certId, pin.clone());
|
||||
logger.debug("PIN salvo em memória de sessão para o certificado '{}'. Tamanho: {} dígitos", certId, pin.length);
|
||||
clearPin(applicationId, certId);
|
||||
sessionPins.put(key, new PinEntry(pin.clone(), Instant.now()));
|
||||
logger.debug("PIN salvo em sessão para app '{}' e cert '{}'. Tamanho: {} dígitos", applicationId, certId, pin.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera o PIN salvo em sessão para o certificado informado.
|
||||
* Salva o PIN no escopo padrão (compatibilidade).
|
||||
*/
|
||||
public synchronized char[] getPin(String certId) {
|
||||
public synchronized void savePin(String certId, char[] pin) {
|
||||
savePin("default", certId, pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera o PIN salvo em sessão para a aplicação e certificado especificados.
|
||||
*/
|
||||
public synchronized char[] getPin(String applicationId, String certId) {
|
||||
if (certId == null || alwaysPromptPin) {
|
||||
return null;
|
||||
}
|
||||
char[] stored = sessionPins.get(certId);
|
||||
return stored != null ? stored.clone() : null;
|
||||
String key = buildKey(applicationId, certId);
|
||||
PinEntry entry = sessionPins.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verifica expiração por inatividade (TTL)
|
||||
if (Instant.now().isAfter(entry.lastAccessedAt().plus(SESSION_TTL_MINUTES, ChronoUnit.MINUTES))) {
|
||||
clearPin(applicationId, certId);
|
||||
logger.info("PIN da sessão expirou por inatividade (TTL {} min) para app '{}' e cert '{}'.", SESSION_TTL_MINUTES, applicationId, certId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Atualiza timestamp de último acesso
|
||||
sessionPins.put(key, new PinEntry(entry.pin(), Instant.now()));
|
||||
return entry.pin().clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se existe um PIN válido salvo nesta sessão para o certificado.
|
||||
* Recupera o PIN no escopo padrão (compatibilidade).
|
||||
*/
|
||||
public synchronized char[] getPin(String certId) {
|
||||
return getPin("default", certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se existe um PIN válido salvo nesta sessão para a aplicação e certificado.
|
||||
*/
|
||||
public synchronized boolean hasPin(String applicationId, String certId) {
|
||||
return getPin(applicationId, certId) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se existe um PIN no escopo padrão (compatibilidade).
|
||||
*/
|
||||
public synchronized boolean hasPin(String certId) {
|
||||
if (certId == null || alwaysPromptPin) {
|
||||
return false;
|
||||
}
|
||||
return sessionPins.containsKey(certId);
|
||||
return hasPin("default", certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove o PIN salvo para um certificado específico, zerando a memória.
|
||||
* Remove o PIN salvo para uma aplicação e certificado específicos, zerando a memória.
|
||||
*/
|
||||
public synchronized void clearPin(String certId) {
|
||||
public synchronized void clearPin(String applicationId, String certId) {
|
||||
if (certId != null) {
|
||||
char[] old = sessionPins.remove(certId);
|
||||
if (old != null) {
|
||||
Arrays.fill(old, ' ');
|
||||
String key = buildKey(applicationId, certId);
|
||||
PinEntry old = sessionPins.remove(key);
|
||||
if (old != null && old.pin() != null) {
|
||||
Arrays.fill(old.pin(), ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove o PIN no escopo padrão (compatibilidade).
|
||||
*/
|
||||
public synchronized void clearPin(String certId) {
|
||||
clearPin("default", certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpa imediatamente todos os PINs em memória e zera seus caracteres.
|
||||
*/
|
||||
public synchronized void clearAll() {
|
||||
for (char[] pin : sessionPins.values()) {
|
||||
if (pin != null) {
|
||||
Arrays.fill(pin, ' ');
|
||||
for (PinEntry entry : sessionPins.values()) {
|
||||
if (entry != null && entry.pin() != null) {
|
||||
Arrays.fill(entry.pin(), ' ');
|
||||
}
|
||||
}
|
||||
sessionPins.clear();
|
||||
|
||||
+46
-7
@@ -11,12 +11,12 @@ import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
@@ -24,13 +24,14 @@ import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
|
||||
* <p>
|
||||
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
|
||||
* a partir de 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}.
|
||||
*/
|
||||
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 SALT_FILE_NAME = "secure-pins.salt";
|
||||
private static final String SEED_FILE_NAME = "secure-pins.seed";
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
private static final int PBKDF2_ITERATIONS = 65536;
|
||||
@@ -49,6 +51,7 @@ public final class SecurePinStorage {
|
||||
private final Path storageDirectory;
|
||||
private final Path dataFile;
|
||||
private final Path saltFile;
|
||||
private final Path seedFile;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -64,6 +67,7 @@ public final class SecurePinStorage {
|
||||
this.storageDirectory = Objects.requireNonNull(storageDirectory);
|
||||
this.dataFile = storageDirectory.resolve(FILE_NAME);
|
||||
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
|
||||
this.seedFile = storageDirectory.resolve(SEED_FILE_NAME);
|
||||
this.objectMapper = new ObjectMapper();
|
||||
loadFromDisk();
|
||||
}
|
||||
@@ -200,6 +204,7 @@ public final class SecurePinStorage {
|
||||
fos.write(iv);
|
||||
fos.write(ciphertext);
|
||||
}
|
||||
restrictFilePermissions(dataFile);
|
||||
} catch (Exception 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];
|
||||
new SecureRandom().nextBytes(salt);
|
||||
Files.write(saltFile, salt);
|
||||
restrictFilePermissions(saltFile);
|
||||
return salt;
|
||||
}
|
||||
|
||||
private byte[] getOrCreateSeed() throws Exception {
|
||||
if (Files.exists(seedFile)) {
|
||||
return Files.readAllBytes(seedFile);
|
||||
}
|
||||
byte[] seed = new byte[32];
|
||||
new SecureRandom().nextBytes(seed);
|
||||
Files.write(seedFile, seed);
|
||||
restrictFilePermissions(seedFile);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private void restrictFilePermissions(Path path) {
|
||||
try {
|
||||
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
|
||||
if (view != null) {
|
||||
Files.setPosixFilePermissions(path, Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE
|
||||
));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Em sistemas não-POSIX como Windows, as permissões são herdadas da pasta de perfil do usuário (~/.cybercore)
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKey deriveKey(byte[] salt) throws Exception {
|
||||
// Combina identificadores estáveis exclusivos da máquina e usuário
|
||||
byte[] seed = getOrCreateSeed();
|
||||
String hostIdentity = String.join(":",
|
||||
System.getProperty("user.name", "unknown"),
|
||||
System.getProperty("user.home", "unknown"),
|
||||
System.getProperty("os.name", "unknown"),
|
||||
System.getProperty("os.arch", "unknown"),
|
||||
"cybercore-signer-hardware-pin-binding"
|
||||
System.getProperty("os.arch", "unknown")
|
||||
);
|
||||
byte[] hostBytes = hostIdentity.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] combinedMaterial = new byte[seed.length + hostBytes.length];
|
||||
System.arraycopy(seed, 0, combinedMaterial, 0, seed.length);
|
||||
System.arraycopy(hostBytes, 0, combinedMaterial, seed.length, hostBytes.length);
|
||||
|
||||
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");
|
||||
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
|
||||
|
||||
Arrays.fill(materialChars, ' ');
|
||||
Arrays.fill(combinedMaterial, (byte) 0);
|
||||
|
||||
return new SecretKeySpec(keyBytes, "AES");
|
||||
}
|
||||
}
|
||||
|
||||
+58
-1
@@ -198,6 +198,63 @@ class CcspServerTest {
|
||||
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
|
||||
@DisplayName("Requisição OPTIONS deve retornar headers CORS válidos")
|
||||
void testCorsPreflight() throws Exception {
|
||||
@@ -398,7 +455,7 @@ class CcspServerTest {
|
||||
assertThat(resp.body()).contains("\"status\":\"success\"");
|
||||
assertThat(resp.body()).contains("\"seals\":");
|
||||
assertThat(resp.body()).contains("icp-brasil");
|
||||
assertThat(resp.body()).contains("gov-br");
|
||||
assertThat(resp.body()).contains("ICP-Brasil Institucional");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+13
@@ -58,4 +58,17 @@ class PinSessionManagerTest {
|
||||
assertThat(manager.hasPin("cert-a")).isFalse();
|
||||
assertThat(manager.hasPin("cert-b")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve isolar PINs entre diferentes aplicações para o mesmo certificado")
|
||||
void shouldIsolatePinByApplicationId() {
|
||||
manager.savePin("app-1", "cert-x", "pin-app-1".toCharArray());
|
||||
|
||||
assertThat(manager.hasPin("app-1", "cert-x")).isTrue();
|
||||
assertThat(manager.getPin("app-1", "cert-x")).isEqualTo("pin-app-1".toCharArray());
|
||||
|
||||
// Aplicação 2 não deve ter acesso ao PIN da Aplicação 1
|
||||
assertThat(manager.hasPin("app-2", "cert-x")).isFalse();
|
||||
assertThat(manager.getPin("app-2", "cert-x")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user