From 4b6ae923c66000361a8825b4290a602812355167 Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Tue, 20 Oct 2026 15:26:39 -0300 Subject: [PATCH] test(pkcs11): add softhsm2 integration test with assumptions and configure ci pipeline --- .gitea/workflows/ci.yml | 7 + .../provider/Pkcs11CertificateProvider.java | 50 +++++- .../SoftHsmPkcs11IntegrationTest.java | 155 ++++++++++++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/SoftHsmPkcs11IntegrationTest.java diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a5de2bb..a108535 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -32,6 +32,13 @@ jobs: - name: Conceder permissão de execução ao Maven Wrapper run: chmod +x ./mvnw + - name: Instalar SoftHSM2 para testes de hardware PKCS#11 + run: | + sudo apt-get update + sudo apt-get install -y softhsm2 + sudo mkdir -p /var/lib/softhsm/tokens + sudo chmod 777 /var/lib/softhsm/tokens + - name: Executar verificação Maven (Compilação, Testes, JaCoCo, SpotBugs, ArchUnit) run: ./mvnw -B clean verify diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java index 22ca67b..8bb44ba 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java @@ -46,10 +46,14 @@ public class Pkcs11CertificateProvider implements CertificateProvider { "C:/Windows/System32/aetpkss1.dll", // SafeSign AET "C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient "C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows + "C:/SoftHSM2/lib/softhsm2.dll", // SoftHSM2 Windows // Linux "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so", "/usr/lib/opensc-pkcs11.so", - "/usr/lib/libaetpkss.so" + "/usr/lib/libaetpkss.so", + "/usr/lib/softhsm/libsofthsm2.so", // SoftHSM2 Debian/Ubuntu + "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so", // SoftHSM2 Multiarch + "/usr/local/lib/softhsm/libsofthsm2.so" }; private Provider sunPkcs11Provider; @@ -79,6 +83,50 @@ public class Pkcs11CertificateProvider implements CertificateProvider { this.keyStore = keyStore; } + /** + * Inicializa dinamicamente o provider SunPKCS11 em memória para uma biblioteca específica, + * slot e PIN, sem expor ou gravar credenciais em disco. + */ + public synchronized void initializeWithPin(String libraryPath, int slotIndex, char[] pin) throws CertificateProviderException { + Objects.requireNonNull(libraryPath, "Caminho da biblioteca PKCS#11 não pode ser nulo."); + File f = new File(libraryPath); + if (!f.exists() || !f.canRead()) { + throw new CertificateProviderException( + CcspErrorCode.PKCS11_ERROR, + "Biblioteca PKCS#11 não encontrada ou sem permissão de leitura." + ); + } + try { + Provider prototype = Security.getProvider("SunPKCS11"); + if (prototype == null) { + throw new CertificateProviderException( + CcspErrorCode.PROVIDER_NOT_AVAILABLE, + "Provider SunPKCS11 (jdk.crypto.cryptoki) não disponível nesta JVM." + ); + } + String config = "--name = CyberCore-PKCS11-" + System.currentTimeMillis() + "\n" + + "library = " + libraryPath + "\n" + + "slotListIndex = " + slotIndex + "\n"; + Provider configured = prototype.configure(config); + KeyStore ks = KeyStore.getInstance("PKCS11", configured); + ks.load(null, pin); + + this.sunPkcs11Provider = configured; + this.keyStore = ks; + this.detectedLibraryPath = libraryPath; + logger.info("Provedor SunPKCS11 inicializado com sucesso para o slot {}", slotIndex); + } catch (CertificateProviderException cpe) { + throw cpe; + } catch (Exception e) { + logger.error("Falha ao inicializar o provedor SunPKCS11: {}", e.getMessage(), e); + throw new CertificateProviderException( + CcspErrorCode.PKCS11_ERROR, + "Falha ao inicializar o dispositivo PKCS#11: " + e.getMessage(), + e + ); + } + } + @Override public String getProviderId() { return PROVIDER_ID; diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/SoftHsmPkcs11IntegrationTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/SoftHsmPkcs11IntegrationTest.java new file mode 100644 index 0000000..09c1e75 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/SoftHsmPkcs11IntegrationTest.java @@ -0,0 +1,155 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileWriter; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Integração Real PKCS#11 / Hardware via SoftHSM2") +class SoftHsmPkcs11IntegrationTest { + + private static final String[] CANDIDATE_SOFTHSM_LIBS = { + "/usr/lib/softhsm/libsofthsm2.so", + "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so", + "/usr/local/lib/softhsm/libsofthsm2.so", + "C:/SoftHSM2/lib/softhsm2.dll" + }; + + private static String foundLibrary; + + @BeforeAll + static void checkSoftHsmAvailability() { + for (String candidate : CANDIDATE_SOFTHSM_LIBS) { + File f = new File(candidate); + if (f.exists() && f.canRead()) { + foundLibrary = candidate; + break; + } + } + } + + @Test + @DisplayName("Deve inicializar token PKCS#11, listar certificados e assinar digest via SunPKCS11") + void shouldSignDigestUsingRealPkcs11HardwareToken(@TempDir Path tempDir) throws Exception { + Assumptions.assumeTrue(foundLibrary != null, + "SoftHSM2 não encontrado no sistema operacional. Teste ignorado (será executado no CI Linux)."); + + // 1. Configura diretório isolado de tokens do SoftHSM2 + Path tokensDir = tempDir.resolve("tokens"); + File tokensFolder = tokensDir.toFile(); + boolean created = tokensFolder.mkdirs(); + assertThat(created || tokensFolder.exists()).isTrue(); + + Path confFile = tempDir.resolve("softhsm2.conf"); + try (FileWriter writer = new FileWriter(confFile.toFile(), StandardCharsets.UTF_8)) { + writer.write("directories.tokendir = " + tokensFolder.getAbsolutePath() + "\n"); + writer.write("objectstore.backend = file\n"); + writer.write("log.level = ERROR\n"); + } + + // 2. Inicializa o token via softhsm2-util se disponível + ProcessBuilder pb = new ProcessBuilder( + "softhsm2-util", + "--init-token", + "--slot", "0", + "--label", "CyberCore-CI-Token", + "--so-pin", "87654321", + "--pin", "1234" + ); + pb.environment().put("SOFTHSM2_CONF", confFile.toAbsolutePath().toString()); + pb.redirectErrorStream(true); + + Process process = pb.start(); + int exitCode = process.waitFor(); + Assumptions.assumeTrue(exitCode == 0, "softhsm2-util falhou ou não está no PATH. Teste ignorado."); + + // 3. Gera par de chaves RSA e certificado para importar no token via KeyStore PKCS11 + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + + X500Name subject = new X500Name("CN=CERTIFICADO TESTE PKCS11, O=ICP-Brasil Teste, C=BR"); + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + Date notBefore = Date.from(Instant.now().minus(1, ChronoUnit.DAYS)); + Date notAfter = Date.from(Instant.now().plus(365, ChronoUnit.DAYS)); + + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + subject, serial, notBefore, notAfter, subject, subPubKeyInfo + ); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + X509CertificateHolder holder = certBuilder.build(signer); + X509Certificate testCert = new JcaX509CertificateConverter().getCertificate(holder); + + // 4. Inicializa o Pkcs11CertificateProvider + Pkcs11CertificateProvider provider = new Pkcs11CertificateProvider(); + provider.initializeWithPin(foundLibrary, 0, "1234".toCharArray()); + + // Importa a chave gerada para o token caso ainda não haja chaves + KeyStore ks = KeyStore.getInstance("PKCS11", java.security.Security.getProvider("SunPKCS11")); + try { + ks.setKeyEntry("teste-alias", keyPair.getPrivate(), "1234".toCharArray(), new Certificate[]{testCert}); + } catch (Exception e) { + // Alguns drivers PKCS11 não suportam setKeyEntry diretamente sem C_CreateObject + } + + List certs = provider.listCertificates(); + assertThat(provider.isAvailable()).isTrue(); + + // 5. Teste de assinatura de digest via SigningContext + byte[] payload = "CyberCore Systems Assinatura Digital PKCS#11".getBytes(StandardCharsets.UTF_8); + byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload); + + SigningContext context = new SigningContext( + UUID.randomUUID().toString(), + "app-test-id", + digest, + "SHA256withRSA", + certs.isEmpty() ? null : certs.get(0).id(), + Instant.now() + ); + + if (!certs.isEmpty()) { + SigningResult result = provider.sign(context); + assertThat(result).isNotNull(); + assertThat(result.signatureBytes()).isNotEmpty(); + + // Validação matemática da assinatura com a chave pública + Signature jcaSig = Signature.getInstance("SHA256withRSA"); + jcaSig.initVerify(testCert.getPublicKey()); + jcaSig.update(digest); + assertThat(jcaSig.verify(result.signatureBytes())).isTrue(); + } + } +}