fix: enhance security with strict file permissions, pin isolation, and robust protocol validations
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ 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 21+)
|
||||||
if ! command -v java &>/dev/null; then
|
if ! command -v java &>/dev/null; then
|
||||||
POSSIBLE_JDKS=(
|
POSSIBLE_JDKS=(
|
||||||
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
"${HOME}/.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/java-21-openjdk-amd64"
|
||||||
"/usr/lib/jvm/default-java"
|
"/usr/lib/jvm/default-java"
|
||||||
|
|||||||
+82
-39
@@ -218,16 +218,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 +354,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 +527,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 +746,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();
|
||||||
@@ -1032,25 +1047,53 @@ public class CcspServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleCors(HttpExchange exchange) throws IOException {
|
private boolean handleCors(HttpExchange exchange) throws IOException {
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
String origin = getOrigin(exchange);
|
String origin = getOrigin(exchange);
|
||||||
|
|
||||||
if (origin == null || origin.isBlank()) {
|
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);
|
// Determina se a origem ou rota é elegível para CORS
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
boolean isHandshakePath = path.endsWith("/status") || path.endsWith("/authorize");
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
boolean isAuthorizedOrigin = applicationRegistry.findByOrigin(origin).isPresent() || isLocalOrigin(origin);
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
|
||||||
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
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()) {
|
||||||
|
|||||||
+76
-26
@@ -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();
|
||||||
|
|||||||
+46
-7
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-1
@@ -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
|
||||||
|
|||||||
+13
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -149,7 +149,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 +255,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ export class CyberCoreSigner {
|
|||||||
* @returns {Promise<Array<Object>>} Lista de certificados com metadados públicos.
|
* @returns {Promise<Array<Object>>} Lista de certificados com metadados públicos.
|
||||||
*/
|
*/
|
||||||
async getCertificates() {
|
async getCertificates() {
|
||||||
const resp = await this._fetch('/ccsp/v1/certificates', {
|
const url = `/ccsp/v1/certificates?application_id=${encodeURIComponent(this.applicationId)}`;
|
||||||
|
const resp = await this._fetch(url, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user