perf(security): implement adaptive smartcard polling and on-demand certificate caching
This commit is contained in:
+66
-4
@@ -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,16 +20,57 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,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());
|
||||||
}
|
}
|
||||||
@@ -76,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,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();
|
||||||
@@ -119,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,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();
|
||||||
@@ -204,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-2
@@ -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)) {
|
||||||
|
|||||||
+47
@@ -93,4 +93,51 @@ class CertificateManagerTest {
|
|||||||
System.clearProperty("cybercore.diag.no-wincert");
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user