perf(security): implement adaptive smartcard polling and on-demand certificate caching

This commit is contained in:
Claudecio Martins
2026-10-20 16:06:29 -03:00
parent e3d56d90af
commit a32ab3b7ce
3 changed files with 132 additions and 6 deletions
@@ -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);
} }
} }
@@ -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)) {
@@ -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();
}
} }